Взаимосвязи эскиза

Немного истории

В начале 2000-х Роберт Мартин, также известный как дядюшка Боб, придумал список из 11 принципов хорошего объектно-ориентированного дизайна. Первые 5 принципов описывали, как сделать хороший дизайн класса. Позже они стали известны под говорящей аббревиатурой SOLID, придуманной Майклом Физерсом. Эти принципы помогают писать надежный, гибкий код.

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

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

Custom Collectors

If you want to write your Collector implementation, you need to implement Collector interface and specify its three generic parameters:

public interface Collector {...}
  1. T – the type of objects that will be available for collection,
  2. A – the type of a mutable accumulator object,
  3. R – the type of a final result.

Let’s write an example Collector for collecting elements into an ImmutableSet instance. We start by specifying the right types:

private class ImmutableSetCollector
  implements Collector, ImmutableSet> {...}

Since we need a mutable collection for internal collection operation handling, we can’t use ImmutableSet for this; we need to use some other mutable collection or any other class that could temporarily accumulate objects for us.
In this case, we will go on with an ImmutableSet.Builder and now we need to implement 5 methods:

  • Supplier> supplier()
  • BiConsumer, T> accumulator()
  • BinaryOperator> combiner()
  • Function, ImmutableSet> finisher()
  • Setcharacteristics()

The supplier() method returns a Supplier instance that generates an empty accumulator instance, so, in this case, we can simply write:

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

The accumulator() method returns a function that is used for adding a new element to an existing accumulator object, so let’s just use the Builder‘s add method.

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

The combiner() method returns a function that is used for merging two accumulators together:

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

The finisher() method returns a function that is used for converting an accumulator to final result type, so in this case, we will just use Builder‘s build method:

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

The characteristics() method is used to provide Stream with some additional information that will be used for internal optimizations. In this case, we do not pay attention to the elements order in a Set so that we will use Characteristics.UNORDERED. To obtain more information regarding this subject, check Characteristics‘ JavaDoc.

@Override public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

Here is the complete implementation along with the usage:

public class ImmutableSetCollector
  implements Collector, ImmutableSet> {

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

@Override
public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static  ImmutableSetCollector toImmutableSet() {
    return new ImmutableSetCollector();
}

and here in action:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet result = givenList.stream()
  .collect(toImmutableSet());

Принцип подстановки Барбары Лисков (Liskov Substitution Principle)

Пожалуй, принцип, который вызывает самые большие затруднения в понимании. Принцип гласит — «Объекты в программе могут быть заменены их наследниками без изменения свойств программы». Своими словами я бы это сказал так — при использовании наследника класса результат выполнения кода должен быть предсказуем и не изменять свойств метод.Типичные примеры нарушения: несогласованное поведение наследников, что приводит к необходимости приводить экземпляры базового класса к конкретным типам наследников.Anti-LSP – Принцип непонятного наследования. Данный анти-принцип проявляется либо в чрезмерном количестве наследования, либо в его полном отсутствии, в зависимости от опыта и взглядов местного главного архитектора.

Liskov Substitution

Next up on our list is Liskov substitution, which is arguably the most complex of the 5 principles. Simply put, if class A is a subtype of class B, then we should be able to replace B with A without disrupting the behavior of our program.

Let’s just jump straight to the code to help wrap our heads around this concept:

public interface Car {

    void turnOnEngine();
    void accelerate();
}

Above, we define a simple Car interface with a couple of methods that all cars should be able to fulfill – turning on the engine, and accelerating forward.

Let’s implement our interface and provide some code for the methods:

public class MotorCar implements Car {

    private Engine engine;

    //Constructors, getters + setters

    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }

    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

As our code describes, we have an engine that we can turn on, and we can increase the power. But wait, its 2019, and Elon Musk has been a busy man.

We are now living in the era of electric cars:

public class ElectricCar implements Car {

    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }

    public void accelerate() {
        //this acceleration is crazy!
    }
}

By throwing a car without an engine into the mix, we are inherently changing the behavior of our program. This is a blatant violation of Liskov substitution and is a bit harder to fix than our previous 2 principles.

One possible solution would be to rework our model into interfaces that take into account the engine-less state of our Car.

Простой пример: Больница

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

Врач может записать пациента к себе на прием, а также принять его:

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

Следуя определению от дядюшки Боба, имплементация класса Doctor должна меняться только при изменении способа лечения пациента. Но в данном примере любые изменения в порядке регистрации также будут менять класс Doctor.

Вынесем функционал регистрации в отдельный класс Registry:

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

А доктора стали заниматься только своей главной обязанностью — лечить людей:

Single Responsibility

Let’s kick things off with the single responsibility principle. As we might expect, this principle states that a class should only have one responsibility. Furthermore, it should only have one reason to change.

How does this principle help us to build better software? Let’s see a few of its benefits:

  1. Testing – A class with one responsibility will have far fewer test cases
  2. Lower coupling – Less functionality in a single class will have fewer dependencies
  3. Organization – Smaller, well-organized classes are easier to search than monolithic ones

Take, for example, a class to represent a simple book:

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters
}

In this code, we store the name, author, and text associated with an instance of a Book.

Let’s now add a couple of methods to query the text:

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

Now, our Book class works well, and we can store as many books as we like in our application. But, what good is storing the information if we can’t output the text to our console and read it?

Let’s throw caution to the wind and add a print method:

public class Book {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

This code does, however, violate the single responsibility principle we outlined earlier. To fix our mess, we should implement a separate class that is concerned only with printing our texts:

public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

Awesome. Not only have we developed a class that relieves the Book of its printing duties, but we can also leverage our BookPrinter class to send our text to other media.

Менее очевидный пример

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

Все методы в классе относятся к бизнес-логике. Поэтому надо и смотреть с точки зрения бизнеса. Если наше приложение используется реальными библиотеками, которые ищут книги и выдают нам реальную физическую книгу, то SPR может быть нарушен.

Мы может заметить, что операциями актера являются те, которые заинтересованны в методах , и . Клиенты так же могут иметь доступ к приложению, чтобы выбрать книгу и прочитать первые несколько страниц для понимания сути книги и чтобы понять, нужна она им или нет. Таким образом актеры — читатели могут быть заинтересованы во всех методах, кроме

Обычному клиенту не важно, где в библиотеке хранится книга. Книга будет передана клиенту библиотекарем

Таким образом мы действительно имеем нарушение SPR.

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

Однако если наш бизнес вдруг решит отказаться от услуг библиотекаря и создать механизм самообслуживания в нашей библиотеке, то тогда можно сказать, что в нашем первом примере SPR не нарушается. Читатели также сами являются и библиотекарями, им нужно самим находить книги. Это также вполне допустимо

Важно здесь помнить, что всегда необходимо тщательно рассмотреть требования бизнеса

3 ответа

49

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

Каждый принцип хорошо объясняется ситуациями реального мира, с которыми может столкнуться каждый разработчик программного обеспечения в своих проектах. Я сокращаю здесь и ссылаюсь на ссылку — S.O.L.I.D. Разработка программного обеспечения, шаг за шагом .

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

Редактировать и комментировать краткую сводку по каждому принципу:

  • «Принцип единой ответственности основан на потребностях бизнеса, чтобы позволить изменения. «Единственная причина для изменения» помогает понять, какие логически отдельные концепции должны быть сгруппированы вместе, рассматривая бизнес-концепцию и контекст, а не только техническую концепцию.
    , я узнал, что каждый класс должен иметь отдельную ответственность.
    Ответственность заключается в том, чтобы просто выполнить назначенную задачу.

  • «Я», я узнал «Открытый закрытый принцип» и начал «предпочитать композицию над наследованием» и как таковой, предпочитая классы, которые не имеют виртуальных методов и, возможно, запечатаны, но зависят от абстракций для их расширения .

  • «L» — «Я изучил принцип замещения Лискова с помощью шаблона репозитория для управления доступом к данным.

  • «Я узнал об Принципе Разделения Интерфейса, узнав, что клиентам не следует принуждать к реализации интерфейсов, которые они не используют (например, в Поставщике Членства в ASP.NET 2.0). Таким образом, интерфейс не должен иметь «много обязанностей»
  • «D» — я узнал о Принципе инверсии зависимостей и начал код, который легко изменить . Простота изменения означает более низкую совокупную стоимость владения и более высокую ремонтопригодность.

9

(I) nterface Segregation и (D) ependency Инверсия может быть изучена посредством модульного тестирования и издевательств. Если классы создают свои собственные зависимости, вы не можете создавать хорошие модульные тесты. Если они зависят от слишком широкого интерфейса (или вообще никакого интерфейса), не совсем очевидно, что нужно издеваться над вашими модульными тестами.

7

Принцип замены Лискова в основном не позволяет вам злоупотреблять наложением реализации: вы никогда не должны использовать наследование только для повторного использования кода (для этого есть состав)! Соблюдая LSP, вы можете быть уверены, что на самом деле существует «is-a relationship» между вашим суперклассом и вашим подклассом.

В нем говорится, что ваши подклассы должны реализовывать все методы подкласса аналогично реализации методов в подклассе. Вы никогда не должны переопределять метод с внедрением NOP или возвращать null, когда супертип выдает исключение; изложенные в разделе «Контракт», вы должны соблюдать договор метода от суперкласса при переопределении метода. Способ защиты от нарушения этого принципа — никогда не отменять реализованный метод; вместо этого извлечь интерфейс и реализовать этот интерфейс в обоих классах.

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

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

В шаблоне Factory вы можете увидеть Инверсия зависимостей ; здесь и компонент высокого уровня (клиент) и низкоуровневый компонент (отдельный экземпляр, который нужно создать) зависят от абстракции ( интерфейс).
Способ применения в многоуровневой архитектуре: вы не должны определять интерфейс для слоя в реализованном слое, а в вызываемом модуле. Например, API для уровня источника данных не должен записываться на уровне источника данных, а в логическом уровне бизнес-уровня, где он должен быть вызван. Таким образом, слой источника данных наследует /зависит от поведения, определенного в логике buisness (таким образом, инверсия), а не наоборот (как это было бы в обычном режиме). Это обеспечивает гибкость в дизайне, позволяя бизнес-логике работать без изменения кода, с другим совершенно другим источником данных.

Признаки плохого кода

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

  1. Закрепощенность — под этим подразумевается, что при попытке внести изменения в один участок кода, возникает необходимость изменить другой участок кода, а за ним третий, пятый и так далее.
  2. Неустойчивость — если при изменении одного участка кода, внезапно перестает работать другой, казалось бы напрямую несвязанный участок.
  3. Неподвижность — невозможность повторного использования написанного ранее кода
  4. Неоправданная сложность — использование слишком сложных синтаксических конструкций, не несущих никакой выгоды
  5. Плохая читаемость — некрасивый и сложный для понимания листинг программы

Open for Extension, Closed for Modification

Now, time for the ‘O’ – more formally known as the open-closed principle. Simply put, classes should be open for extension, but closed for modification. In doing so, we stop ourselves from modifying existing code and causing potential new bugs in an otherwise happy application.

Of course, the one exception to the rule is when fixing bugs in existing code.

Let’s explore the concept further with a quick code example. As part of a new project, imagine we’ve implemented a Guitar class.

It’s fully fledged and even has a volume knob:

public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

We launch the application, and everyone loves it. However, after a few months, we decide the Guitar is a little bit boring and could do with an awesome flame pattern to make it look a bit more ‘rock and roll’.

At this point, it might be tempting to just open up the Guitar class and add a flame pattern – but who knows what errors that might throw up in our application.

Instead, let’s stick to the open-closed principle and simply extend our Guitar class:

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

By extending the Guitar class we can be sure that our existing application won’t be affected.

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

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

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

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

Принцип инверсии зависимостей (D)

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

Внедрение зависимостей может быть выполнено несколькими путями. Рассмотрим их на примере подсистемы уведомлений. Для этого напишем интерфейс для рассылки сообщений и пару классов – реализаций.

А теперь рассмотрим через что же можно внедрить зависимость.

Конструктор.

Свойства.

Метод.

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

Interface Segregation

The ‘I ‘ in SOLID stands for interface segregation, and it simply means that larger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.

For this example, we’re going to try our hands as zookeepers. And more specifically, we’ll be working in the bear enclosure.

Let’s start with an interface that outlines our roles as a bear keeper:

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

As avid zookeepers, we’re more than happy to wash and feed our beloved bears. However, we’re all too aware of the dangers of petting them. Unfortunately, our interface is rather large, and we have no choice than to implement the code to pet the bear.

Let’s fix this by splitting our large interface into 3 separate ones:

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

Now, thanks to interface segregation, we’re free to implement only the methods that matter to us:

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

And finally, we can leave the dangerous stuff to the crazy people:

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

Going further, we could even split our  class from our example earlier to use interface segregation in the same way. By implementing a Printer interface with a single print method, we could instantiate separate ConsoleBookPrinter and OtherMediaBookPrinter classes.

DIP — Dependency Inversion Principle

В любой объектно-ориентированной программе существуют зависимости (связи, отношения) между классами. Очевидно, что с ростом количества и силы зависимостей программа становится менее гибкой. Принцип инверсии зависимостей направлен на повышение гибкости программы за счет ослабления связности классов. Ряд источников утверждает, что суть DIP заключается в замене композиции агрегацией, мы рассмотрим это более детально.

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

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

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

Strategy pattern example

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

Само по себе использование агрегации вместо композиции решило бы не все проблемы, т.к. новый класс должен был бы наследовать TextDecription, но наследование нарушало бы принцип подстановки Лисков, ведь алгоритм шифрования DES не является разновидностью алгоритма XOR. Чтобы оба принципа были соблюдены — необходимо создавать зависимость от абстрактного класса.

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

Подобная замена композиции агрегацией лежит в основе шаблона проектирования стратегия (Strategy) , однако принцип DIP является более общим. Согласно формулировке  принципа инверсии зависимостей от Роберта Мартина:

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

Dependency Inversion Principle example

В верхней части рисунка приведена диаграмма, на которой персонаж умеет обрабатывать игровые элементы любого типа. Клиентский код выбирает очередную клетку на пути движения персонажа и передает ее для обработки, в результате каким-либо образом изменяется состояние. Между конкретными классами артефактов и персонажем имеются зависимости в связи с этим нарушается принцип DIP, в следствии этого осложняется сопровождение кода — при добавлении нового типа игрового объекта, изменения должны коснуться и класса Person.

В нижней части слайда показано другое решение — теперь клиентский код передает состояние персонажа артефакту для обработки. Зависимости между артефактами и состоянием персонажа не нарушают принцип инверсии зависимостей если мы уверены, что не будем создавать разновидности состояний (наследовать класс State). Объекты таких классов, как State называются объектами данных, зависимости от них также не страшны, как и от классов стандартной библиотеки.

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

Что такое SOLID?

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

Ссылка на основную публикацию