Пример соблюдения принципов SOLID

Tags: Solid Principles, Software Development

В этом посте мы обсудим, как мы можем использовать принципы SOLID для написания «Чистого кода».

Плохой код:

  1. Трудно отладить
  2. Трудно понять
  3. Сложно расширить или изменить
  4. Изменение одной функции нарушает другие
  5. Трудно проверить

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

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

У класса или функции должна быть одна-единственная причина для изменения

import java.io.FileWriter;


public class Blog {

    String title = "Microservices";

    String author = "Feynmann";


    public String getTitle() {

        return title;

    


    public String getAuthor() {

        return author;

    


    public void printBlog() {

        System.out.println("prints blog");

    


    public void persist() {

        try {

            FileWriter fw = new FileWriter(getTitle() + "-" + getAuthor() + ".txt");

            fw.write(getTitle() + "-" + getAuthor());

            fw.close();

        } catch (Exception e) {

            System.out.println(e);

        

    

}


Что мы думаем о вышеуказанном классе? Давайте предположим, что это часть системы блогов. В чем проблема с этим классом? «Управление контентом» захочет обновлений, если есть изменения в контенте, «Управление отображением» захочет обновлений, если мы изменим то, как мы отображаем блог, и, наконец, «Управление сохранением» захочет обновлений, если мы решим сохранить бло другим путем. Никогда не бывает хорошо, если слишком много людей имеет долю в одном классе. Какова ответственность этого класса? Сохраняйте информацию в блоге, печатайте блог и сохраняйте блог. Как мы видим, здесь явно более одной ответственности.

Может быть сложно определить единственную ответственность, поскольку вы видите, что все функции связаны с блогом. Хитрость заключается в том, чтобы подумать о разных объектах, которые могут быть заинтересованы в изменении этого класса. Таким образом, если существует более 1 стороны, заинтересованной в изменении класса или функции, мы явно нарушаем SRP. В нашем случае это 3: управление контентом, управление отображением и управление сохранением. Остерегайтесь использования “and then”, чтобы добавить больше ответственности к вашему классу или функции и заставить себя поверить, что это несет единственную ответственность.

Давайте проведем рефакторинг:

package com.dnivra26;


class Auto {


    public int getFrontTyreAir() {

        return frontTyreAir;

    


    public int getBackLeftTyreAir() {

        return backLeftTyreAir;


    public int getBackRightTyreAir() {

        return backRightTyreAir;

    


    public Auto(int frontTyreAir, int backLeftTyreAir, int backRightTyreAir) {

        this.frontTyreAir = frontTyreAir;

        this.backLeftTyreAir = backLeftTyreAir;

        this.backRightTyreAir = backRightTyreAir;

   


    private int frontTyreAir, backLeftTyreAir, backRightTyreAir;


}


class Bike {


    public int getFrontTyreAir() {

        return frontTyreAir;

    


    public int getBackTyreAir() {

        return backTyreAir;

    


    public Bike(int frontTyreAir, int backTyreAir) {

        this.frontTyreAir = frontTyreAir;

        this.backTyreAir = backTyreAir;

    


    private int frontTyreAir, backTyreAir;


}



public class AirChecker {


    public int findAirLeft(Object vehicle) {

        if (vehicle.getClass() == Auto.class) {

            return ((Auto) vehicle).getBackLeftTyreAir() + ((Auto) vehicle).getBackRightTyreAir() + ((Auto) vehicle).getFrontTyreAir();

        } else {

            return ((Bike) vehicle).getBackTyreAir() + ((Bike) vehicle).getFrontTyreAir();

        


    public static void main(String[] args) {

        AirChecker airChecker = new AirChecker();

        Auto auto = new Auto(1, 2, 3);

        Bike bike = new Bike(1, 2);

        System.out.println(airChecker.findAirLeft(auto));

        System.out.println(airChecker.findAirLeft(bike));


}


Цель программы - найти оставшийся воздух (Air) в каждом из наших транспортных средств (vehicle). Что происходит, когда мы хотим добавить новое транспортное средство, скажем, велосипед (Cycle)? Мы должны добавить еще одно “if else” и изменить функцию «findAirLeft». Это еще не все. Эта лестничная структура “if else” будет повторяться во всех местах, где мы хотим использовать некоторую логику в зависимости от типа транспортного средства. Как видите, мы не можем расширить программу без изменений, и для небольшого изменения нам, возможно, придется изменить много кода.

Давайте проведем рефакторинг:

package com.dnivra26;


abstract class Vehicle {

    abstract public int findAir();

}


class Auto extends Vehicle {


    public int getFrontTyreAir() {

        return frontTyreAir;

    


    public int getBackLeftTyreAir() {

        return backLeftTyreAir;

    


    public int getBackRightTyreAir() {

        return backRightTyreAir;

    


    public Auto(int frontTyreAir, int backLeftTyreAir, int backRightTyreAir) {

        this.frontTyreAir = frontTyreAir;

        this.backLeftTyreAir = backLeftTyreAir;

        this.backRightTyreAir = backRightTyreAir;

    



    private int frontTyreAir, backLeftTyreAir, backRightTyreAir;


    @Override

    public int findAir() {

        return frontTyreAir + backLeftTyreAir + backRightTyreAir;

    

}


class Bike extends Vehicle {


    public int getFrontTyreAir() {

        return frontTyreAir;

    


    public int getBackTyreAir() {

        return backTyreAir;

    


    public Bike(int frontTyreAir, int backTyreAir) {

        this.frontTyreAir = frontTyreAir;

        this.backTyreAir = backTyreAir;

    


    private int frontTyreAir, backTyreAir;


    @Override

    public int findAir() {

        return frontTyreAir + backTyreAir;

    

}



public class AirChecker {


    public int findAirLeft(Vehicle vehicle) {

        return vehicle.findAir();

    


    public static void main(String[] args) {

        AirChecker airChecker = new AirChecker();

        Vehicle auto = new Auto(1, 2, 3);

        Vehicle bike = new Bike(1, 2);

        System.out.println(airChecker.findAirLeft(auto));

        System.out.println(airChecker.findAirLeft(bike));

    


}


Теперь, если вы видите наш рефакторированный код, то у нас нет структуры “if else”, и у нас есть абстрактный класс Vehicle с методом findAir, который реализуют все наши транспортные средства. Если мы хотим добавить другое транспортное средство (extend), нам не нужно трогать существующую функциональность (оставляем без изменений).

Принцип замещения Лискова

Вы должны иметь возможность заменить суперкласс подклассом в любом месте программы без каких-либо неожиданных действий.

 

class List {

    public void add(){


    

    public int get() {


    

}


class Stack extends List{

    @Override

    public void add() {

        super.add();

    


    @Override

    public int get() {

        return super.get();

    

}


public class LSP {

    public static void main(String[] args) {

        List list = new List();

        Stack stack = new Stack();

        List myList = new Stack();

    

}

Если мы посмотрим на пример, у нас есть «List», который хранит ряд значений. Теперь мы хотим реализовать «Stack», и мы видим, что у нас уже есть класс, хранящий несколько значений. Мы решаем повторно использовать функциональность, расширяем класс List и переопределяем методы get и put, чтобы следовать LIFO (вот как работает стек).

Что возможно могло пойти не так? Посмотрите на строку № 26. Что происходит, когда мы передаем объект Stack в виде List другому модулю. Кто-то будет использовать его как List, и все будет идти на жеребьевку, потому что List и Stack не работают одинаково.

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

Давайте проведем рефакторинг

class List {

    public void add(){


    

    public int get() {


    

}


class Stack {

    List list;


    public void add() {

        

    

    

    public int get() {

        

    

}


Тем не менее мы снова используем функциональность «List». Но сейчас у нас нет наследования. В этом случае никто не пострадает, потому что нет путаницы.

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

Создавайте простые, сфокусированные интерфейсы вместо больших и раздутых.

 

public interface Clock {

    public void setTime();

    public int getTime();

    public void setAlarm();

    public void getAlarm();

    public void setRadio();

    public void getRadio();

}


class AlarmClock implements Clock{


    @Override

    public void setTime() {

        // does not apply

    


    @Override

    public int getTime() {

        return 0;

    


    @Override

    public void setAlarm() {

        //actual function

    


    @Override

    public void getAlarm() {

        // return alarm

    


    @Override

    public void setRadio() {

        // does not apply

    


    @Override

    public void getRadio() {

        // does not apply

    

}

В чем проблема с приведенным выше фрагментом кода? Интерфейс Clock определяет все функции часов. Проблема в том, что Alarm Clock не нужны все функции, ему нужны только “set alarm” и “get alarm”, но он вынужден реализовывать другие ненужные функции, либо возвращая ноль, либо оставляя функцию пустой.

Давайте проведем рефакторинг

package com.dnivra26;


interface Time {

    public void setTime();


    public int getTime();

}


interface Alarm {

    public void setAlarm();


    public void getAlarm();

}


interface Radio {

    public void setRadio();


    public void getRadio();

}


class AlarmClock implements Alarm {



    @Override

    public void setAlarm() {

        //actual function

    


    @Override

    public void getAlarm() {

        // return alarm

    

    

}

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

Принцип обращения зависимостей

Класс не должен зависеть от другого конкретного класса, вместо этого он должен зависеть от абстракции

class BookDB {

    public void save(Book book) {

        try {

            FileWriter fw = new FileWriter("books.txt");

            fw.write(book.getTitle() + "-" + book.getAuthor());

            fw.close();

        } catch (Exception e) {

            System.out.println(e);

        

        System.out.println("Success...");

    

}

 

Проблема этого класса заключается в том, что когда мы решаем переключиться на другую форму хранения, например, на базу данных SQL, или если FileWriter меняет некоторые из своих API, этот класс должен измениться. Класс BookDB зависит от конкретного класса FileWriter. Тесная связь никогда не бывает хорошей идеей.

Давайте проведем рефакторинг

class BookDB {

    BookPersist bookPersist;

    public void save(Book book) {

        bookPersist.save(book);

    

}


interface BookPersist {

    public void save(Book book);

}


class FilePersist implements BookPersist {


    @Override

    public void save(Book book) {

        try {

            FileWriter fw = new FileWriter("books.txt");

            fw.write(book.getTitle() + "-" + book.getAuthor());

            fw.close();

        } catch (Exception e) {

            System.out.println(e);

        

        System.out.println("Success...");

    

}


Итак, мы вытащили интерфейс с нашей подписью метода «save», и модуль FilePersist реализует ее. Теперь класс BookDB зависит от абстракции, а не напрямую от конкретной реализации FilePersist. С этим изменением изменения модуля более низкого уровня не повлияют на BookDB, а также мы можем легко переключить метод персистентности, внедрив некоторую другую реализацию интерфейса BookPersist (слабая связь).

Если вы внимательно посмотрите, то увидите, что принцип открытости/закрытости и инверсии зависимости действуют заодно. Следуя «Инверсии зависимости», мы можем избежать нарушения принципа открытости/закрытости.

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

SOLID - это лишь один из многих принципов написания «Чистого кода». Есть и другие, такие как KISS, DRY, YAGNI и т. д. Мы можем поговорить о них в одном из следующих постов.

No Comments

Add a Comment