Skip to content

Latest commit

 

History

History
585 lines (469 loc) · 78.1 KB

File metadata and controls

585 lines (469 loc) · 78.1 KB

Вопросы для собеседования

Шаблоны проектирования

Что такое «паттерн проектирования»?

Паттерн (шаблон) проектирования (design pattern) — это проверенное и готовое к использованию решение. Это не класс и не библиотека, которую можно подключить к проекту, это нечто большее - он не зависит от языка программирования, не является законченным образцом, который может быть прямо преобразован в код и может быть реализован по-разному в разных языках программирования. Паттерн проектирования — это часто встречающееся решение определённой проблемы при проектировании архитектуры программ. В отличие от готовых функций или библиотек, паттерн нельзя просто взять и скопировать в программу. Паттерн представляет собой не какой-то конкретный код, а общую концепцию решения той или иной проблемы, которую нужно будет ещё подстроить под нужды вашей программы. Паттерны часто путают с алгоритмами, ведь оба понятия описывают типовые решения каких-то известных проблем. Но если алгоритм — это чёткий набор действий, то паттерн — это высокоуровневое описание решения, реализация которого может отличаться в двух разных программах. Если привести аналогии, то алгоритм — это кулинарный рецепт с чёткими шагами, а паттерн — инженерный чертёж, на котором нарисовано решение, но не конкретные шаги его реализации.

Плюсы использования шаблонов:

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

Минусы:

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

к оглавлению

Из чего состоит паттерн?

Описания паттернов обычно очень формальны и чаще всего состоят из таких пунктов:

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

Назовите основные характеристики шаблонов.

  • Имя - все шаблоны имеют уникальное имя, служащее для их идентификации;
  • Назначение назначение данного шаблона;
  • Задача - задача, которую шаблон позволяет решить;
  • Способ решения - способ, предлагаемый в шаблоне для решения задачи в том контексте, где этот шаблон был найден;
  • Участники - сущности, принимающие участие в решении задачи;
  • Следствия - последствия от использования шаблона как результат действий, выполняемых в шаблоне;
  • Реализация - возможный вариант реализации шаблона.

к оглавлению

Типы шаблонов проектирования. [Middle+]

  • Основные (Fundamental) - основные строительные блоки других шаблонов. Большинство других шаблонов использует эти шаблоны в той или иной форме.
  • Порождающие шаблоны (Creational) — шаблоны проектирования, которые абстрагируют процесс создание экземпляра. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов. Шаблон, порождающий классы, использует наследование, чтобы изменять созданный объект, а шаблон, порождающий объекты, делегирует создание объектов другому объекту.
  • Структурные шаблоны (Structural) определяют различные сложные структуры, которые изменяют интерфейс уже существующих объектов или его реализацию, позволяя облегчить разработку и оптимизировать программу.
  • Поведенческие шаблоны (Behavioral) определяют взаимодействие между объектами, увеличивая таким образом его гибкость.

к оглавлению

Приведите примеры основных шаблонов проектирования.

  • Фабричный метод (Factory Method) - это шаблон, который используется для создания объектов без необходимости определения их конкретных классов в коде. Вместо этого используется метод фабрики, который определяет, какой тип объекта должен быть создан на основе заданных параметров. Например, фабрика может создавать объекты различных классов, в зависимости от входных параметров, таких как тип, размер и цвет.

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

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

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

  • Делегирование (Delegation pattern) - Сущность внешне выражает некоторое поведение, но в реальности передаёт ответственность за выполнение этого поведения связанному объекту.

  • Функциональный дизайн (Functional design) - Гарантирует, что каждая сущность имеет только одну обязанность и исполняет её с минимумом побочных эффектов на другие.

  • Неизменяемый интерфейс (Immutable interface) - Создание неизменяемого объекта.

  • Интерфейс (Interface) - Общий метод структурирования сущностей, облегчающий их понимание.

  • Интерфейс-маркер (Marker interface) - В качестве атрибута (как пометки объектной сущности) применяется наличие или отсутствие реализации интерфейса-маркера. В современных языках программирования вместо этого применяются атрибуты или аннотации.

  • Контейнер свойств (Property container) - Позволяет добавлять дополнительные свойства сущности в контейнер внутри себя, вместо расширения новыми свойствами.

  • Канал событий (Event channel) - Создаёт централизованный канал для событий. Использует сущность-представитель для подписки и сущность-представитель для публикации события в канале. Представитель существует отдельно от реального издателя или подписчика. Подписчик может получать опубликованные события от более чем одной сущности, даже если он зарегистрирован только на одном канале.

к оглавлению

Приведите примеры порождающих шаблонов проектирования.

  • Абстрактная фабрика (Abstract factory) - Класс, который представляет собой интерфейс для создания других классов.
  • Строитель (Builder) - Класс, который представляет собой интерфейс для создания сложного объекта.
  • Фабричный метод (Factory method) - Делегирует создание объектов наследникам родительского класса. Это позволяет использовать в коде программы не специфические классы, а манипулировать абстрактными объектами на более высоком уровне.
  • Прототип (Prototype) - Определяет интерфейс создания объекта через клонирование другого объекта вместо создания через конструктор.
  • Одиночка (Singleton) - Класс, который может иметь только один экземпляр.

к оглавлению

Приведите примеры структурных шаблонов проектирования.

  • Адаптер (Adapter) - Объект, обеспечивающий взаимодействие двух других объектов, один из которых использует, а другой предоставляет несовместимый с первым интерфейс.
  • Мост (Bridge) - Структура, позволяющая изменять интерфейс обращения и интерфейс реализации класса независимо.
  • Компоновщик (Composite) - Объект, который объединяет в себе объекты, подобные ему самому.
  • Декоратор (Decorator) - Класс, расширяющий функциональность другого класса без использования наследования.
  • Фасад (Facade) - Объект, который абстрагирует работу с несколькими классами, объединяя их в единое целое.
  • Приспособленец (Flyweight) - Это объект, представляющий себя как уникальный экземпляр в разных местах программы, но по факту не являющийся таковым.
  • Заместитель (Proxy) - Объект, который является посредником между двумя другими объектами, и который реализует/ограничивает доступ к объекту, к которому обращаются через него.

к оглавлению

Приведите примеры поведенческих шаблонов проектирования.

  • Цепочка обязанностей (Chain of responsibility) - Предназначен для организации в системе уровней ответственности.
  • Команда (Command) - Представляет действие. Объект команды заключает в себе само действие и его параметры.
  • Интерпретатор (Interpreter) - Решает часто встречающуюся, но подверженную изменениям, задачу.
  • Итератор (Iterator) - Представляет собой объект, позволяющий получить последовательный доступ к элементам объекта-агрегата без использования описаний каждого + __из объектов, входящих в состав агрегации.
  • Посредник (Mediator) - Обеспечивает взаимодействие множества объектов, формируя при этом слабую связанность и избавляя объекты от необходимости явно ссылаться друг на друга.
  • Хранитель (Memento) - Позволяет, не нарушая инкапсуляцию зафиксировать и сохранить внутренние состояния объекта так, чтобы позднее восстановить его в этих состояниях.
  • Наблюдатель (Observer) - Определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом событии.
  • Состояние (State) - Используется в тех случаях, когда во время выполнения программы объект должен менять своё поведение в зависимости от своего состояния.
  • Стратегия (Strategy) - Предназначен для определения семейства алгоритмов, инкапсуляции каждого из них и обеспечения их взаимозаменяемости.
  • Шаблонный метод (Template method) - Определяет основу алгоритма и позволяет наследникам переопределять некоторые шаги алгоритма, не изменяя его структуру в целом.
  • Посетитель (Visitor) - Описывает операцию, которая выполняется над объектами других классов. При изменении класса Visitor нет необходимости изменять обслуживаемые классы.

к оглавлению

Расскажите о паттерне Singleton

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

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

  • Гарантирует наличие единственного экземпляра класса. Чаще всего это полезно для доступа к какому-то общему ресурсу, например, базе данных. Представьте, что вы создали объект, а через некоторое время пробуете создать ещё один. В этом случае хотелось бы получить старый объект, вместо создания нового. Такое поведение невозможно реализовать с помощью обычного конструктора, так как конструктор класса всегда возвращает новый объект.
  • Предоставляет глобальную точку доступа. Это не просто глобальная переменная, через которую можно достучаться к определённому объекту. Глобальные переменные не защищены от записи, поэтому любой код может подменять их значения без вашего ведома. Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс. Интересно, что в наше время паттерн стал настолько известен, что теперь люди называют «одиночками» даже те классы, которые решают лишь одну из проблем, перечисленных выше.

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

Структура

image

Применимость

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

Шаги реализации

  • Добавьте в класс приватное статическое поле, которое будет содержать одиночный объект.
  • Объявите статический создающий метод, который будет использоваться для получения одиночки.
  • Добавьте «ленивую инициализацию» (создание объекта при первом вызове метода) в создающий метод одиночки.
  • Сделайте конструктор класса приватным.
  • В клиентском коде замените вызовы конструктора одиночка вызовами его создающего метода.

Преимущества

  • Гарантирует наличие единственного экземпляра класса.
  • Предоставляет к нему глобальную точку доступа.
  • Реализует отложенную инициализацию объекта-одиночки.

Недостатки

  • Нарушает принцип единственной ответственности класса.
  • Маскирует плохой дизайн.
  • Проблемы мультипоточности.
  • Требует постоянного создания Mock-объектов при юнит-тестировании.

Способы реализации паттерна Singleton

  • Самый простой подход - использовать статический экземпляр класса с закрытым конструктором, чтобы гарантировать, что объект класса создается только один раз и его можно получить через метод getInstance(). Вот пример:
public class Singleton {
    private static Singleton instance;

    private Singleton() { // закрытый конструктор
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

В этом примере getInstance() проверяет, существует ли экземпляр Singleton, и создает его, если нет. Этот подход может не работать в многопоточной среде, так как два потока могут одновременно проверить, что instance равно null, и создать два разных экземпляра.

  • Другой подход - использовать двойную проверку на null в методе getInstance() и синхронизировать его, чтобы гарантировать, что только один поток может создавать экземпляр. Вот пример:
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() { // закрытый конструктор
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

В этом примере использована двойная проверка на null для гарантии создания единственного экземпляра класса, и переменная instance объявлена как volatile для обеспечения правильного многопоточного поведения. Синхронизация обеспечивает правильную работу в многопоточной среде, но может снизить производительность в случае высокой нагрузки на приложение.

  • Еще один подход - использовать перечисление (enum) для определения Singleton. Перечисления в Java являются singleton-ами по умолчанию, и каждый элемент перечисления создается только один раз. Вот пример:
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // реализация метода
    }
}

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

Приведите примеры использования паттерна Singleton

  • Логгирование - для создания единственного объекта логгера, который может использоваться во всем приложении, и записи сообщений лога в единое место.
  • Работа с базой данных - для создания единственного объекта соединения с базой данных, который может использоваться во всем приложении, и обеспечения многопоточной безопасности и оптимизации ресурсов.
  • Работа с файлами конфигурации - для создания единственного объекта файла конфигурации, который может использоваться во всем приложении, и чтения настроек из единого места.
  • Работа с кэшем - для создания единственного объекта кэша, который может использоваться во всем приложении, и обеспечения многопоточной безопасности и оптимизации ресурсов при кэшировании данных.
  • Игровые приложения - для создания единственного объекта игрового движка, который может использоваться во всех частях игры и обеспечивать единую логику и настройки игровой среды.
  • Создание объекта управления ресурсами - для создания единственного объекта управления ресурсами, который может использоваться во всем приложении, и обеспечивать управление ресурсами, такими как память, потоки и файловая система.
  • Работа с API - для создания единственного объекта API-клиента, который может использоваться во всем приложении, и обеспечения многопоточной безопасности и оптимизации ресурсов при обращении к API.
  • Кеширование результатов - для создания единственного объекта кеша, который может использоваться во всем приложении, и обеспечения быстрого доступа к результатам предыдущих операций.

Расскажите о паттерне Decorator

Decorator — это структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки» (другой объект с таким же интерфейсом). Этот шаблон позволяет добавлять новые возможности, не изменяя уже существующий код. Каждый декоратор имеет тот же интерфейс, что и исходный объект, поэтому они могут быть вложены друг в друга.

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

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

image

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

Применимость

  • Когда вам нужно добавлять обязанности объектам на лету, незаметно для кода, который их использует. Объекты помещают в обёртки, имеющие дополнительные поведения. Обёртки и сами объекты имеют одинаковый интерфейс, поэтому клиентам без разницы, с чем работать — с обычным объектом данных или с обёрнутым.
  • Когда нельзя расширить обязанности объекта с помощью наследования. Во многих языках программирования есть ключевое слово final, которое может заблокировать наследование класса. Расширить такие классы можно только с помощью Декоратора.

Шаги реализации

  • Убедитесь, что в вашей задаче есть один основной компонент и несколько опциональных дополнений или надстроек над ним.
  • Создайте интерфейс компонента, который описывал бы общие методы как для основного компонента, так и для его дополнений.
  • Создайте класс конкретного компонента и поместите в него основную бизнес-логику.
  • Создайте базовый класс декораторов. Он должен иметь поле для хранения ссылки на вложенный объект-компонент. Все методы базового декоратора должны делегировать действие вложенному объекту.
  • И конкретный компонент, и базовый декоратор должны следовать одному и тому же интерфейсу компонента.
  • Теперь создайте классы конкретных декораторов, наследуя их от базового декоратора. Конкретный декоратор должен выполнять свою добавочную функцию, а затем (или перед этим) вызывать эту же операцию обёрнутого объекта.
  • Клиент берёт на себя ответственность за конфигурацию и порядок обёртывания объектов.

Преимущества

  • Большая гибкость, чем у наследования.
  • Позволяет добавлять обязанности на лету.
  • Можно добавлять несколько новых обязанностей сразу.
  • Позволяет иметь несколько мелких объектов вместо одного объекта на все случаи жизни.

Недостатки

  • Трудно конфигурировать многократно обёрнутые объекты.
  • Обилие крошечных классов.

Приведите пример использования шаблона Decorator

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

Вот пример реализации шаблона Decorator на языке Java:

// Интерфейс, определяющий общий интерфейс для объектов и декораторов
public interface TextEditor {
    public String getText();
    public void setText(String text);
}

// Базовый текстовый редактор
public class BasicTextEditor implements TextEditor {
    private String text;

    public String getText() {
        return text;
    }
    public void setText(String text) {
        this.text = text;
    }
}

// Абстрактный декоратор
public abstract class TextEditorDecorator implements TextEditor {
    private TextEditor editor;

    public TextEditorDecorator(TextEditor editor) {
        this.editor = editor;
    }

    public String getText() {
        return editor.getText();
    }
    public void setText(String text) {
        editor.setText(text);
    }
}

// Декоратор проверки орфографии
public class SpellCheckDecorator extends TextEditorDecorator {
    public SpellCheckDecorator(TextEditor editor) {
        super(editor);
    }

    public String getText() {
        String text = super.getText();
        // проверка орфографии
        return text;
    }
}

// Декоратор автокоррекции
public class AutoCorrectDecorator extends TextEditorDecorator {
    public AutoCorrectDecorator(TextEditor editor) {
        super(editor);
    }

    public void setText(String text) {
        // автокоррекция текста
        super.setText(text);
    }
}

В этом примере интерфейс TextEditor определяет общий интерфейс для базового текстового редактора и декораторов. Класс BasicTextEditor является базовым текстовым редактором, который реализует этот интерфейс.

Вот еще несколько примеров использования паттерна Decorator:

  • Кофейная машина - для создания декораторов, которые добавляют к кофе новые вкусовые свойства, например, сиропы, взбитые сливки и т.д.
  • Парсер - для создания декораторов, которые добавляют к парсеру новые возможности, такие как проверка правописания, автокоррекция, конвертирование форматов и т.д.
  • Графический интерфейс - для создания декораторов, которые добавляют к окнам новые возможности, такие как прокрутка, управление размерами, анимации и т.д.
  • Файловый поток - для создания декораторов, которые добавляют к файловым потокам новые функциональные возможности, такие как шифрование, сжатие, буферизация и т.д.
  • Рисование - для создания декораторов, которые добавляют к рисованию новые возможности, такие как текстуры, шейдеры, эффекты и т.д.
  • Музыкальный плеер - для создания декораторов, которые добавляют к музыкальному плееру новые функциональные возможности, такие как эквалайзер, визуализация, плейлисты и т.д.
  • Архиватор - для создания декораторов, которые добавляют к архиватору новые возможности, такие как шифрование, сжатие, проверка целостности и т.д.
  • Сетевые соединения - для создания декораторов, которые добавляют к сетевым соединениям новые возможности, такие как управление буфером, аутентификация, шифрование и т.д.

Расскажите о паттерне Factory Method

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

Проблема

Представьте, что вы создаёте программу управления грузовыми перевозками. Сперва вы рассчитываете перевозить товары только на автомобилях. Поэтому весь ваш код работает с объектами класса Грузовик. В какой-то момент ваша программа становится настолько известной, что морские перевозчики выстраиваются в очередь и просят добавить поддержку морской логистики в программу. Большая часть существующего кода жёстко привязана к классам Грузовиков. Чтобы добавить в программу классы морских Судов, понадобится перелопатить всю программу. Более того, если вы потом решите добавить в программу ещё один вид транспорта, то всю эту работу придётся повторить. В итоге вы получите ужасающий код, наполненный условными операторами, которые выполняют то или иное действие, в зависимости от класса транспорта.

Решение

Паттерн Фабричный метод предлагает создавать объекты не напрямую, используя оператор new, а через вызов особого фабричного метода. Не пугайтесь, объекты всё равно будут создаваться при помощи new, но делать это будет фабричный метод.

image

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

image

Например, классы Грузовик и Судно реализуют интерфейс Транспорт с методом доставить. Каждый из этих классов реализует метод по-своему: грузовики везут грузы по земле, а суда — по морю. Фабричный метод в классе ДорожнойЛогистики вернёт объект-грузовик, а класс МорскойЛогистики — объект-судно. Для клиента фабричного метода нет разницы между этими объектами, так как он будет трактовать их как некий абстрактный Транспорт. Для него будет важно, чтобы объект имел метод доставить, а как конкретно он работает — не важно.

image

Шаги реализации

  • Приведите все создаваемые продукты к общему интерфейсу.
  • В классе, который производит продукты, создайте пустой фабричный метод. В качестве возвращаемого типа укажите общий интерфейс продукта.
  • Затем пройдитесь по коду класса и найдите все участки, создающие продукты. Поочерёдно замените эти участки вызовами фабричного метода, перенося в него код создания различных продуктов.
  • В фабричный метод, возможно, придётся добавить несколько параметров, контролирующих, какой из продуктов нужно создать.
  • На этом этапе фабричный метод, скорее всего, будет выглядеть удручающе. В нём будет жить большой условный оператор, выбирающий класс создаваемого продукта. Но не волнуйтесь, мы вот-вот исправим это.
  • Для каждого типа продуктов заведите подкласс и переопределите в нём фабричный метод. Переместите туда код создания соответствующего продукта из суперкласса.
  • Если создаваемых продуктов слишком много для существующих подклассов создателя, вы можете подумать о введении параметров в фабричный метод, которые позволят возвращать различные продукты в пределах одного подкласса.
  • Например, у вас есть класс Почта с подклассами АвиаПочта и НаземнаяПочта, а также классы продуктов Самолёт, Грузовик и Поезд. Авиа соответствует Самолётам, но для НаземнойПочты есть сразу два продукта. Вы могли бы создать новый подкласс почты для поездов, но проблему можно решить и по-другому. Клиентский код может передавать в фабричный метод НаземнойПочты аргумент, контролирующий тип создаваемого продукта.
  • Если после всех перемещений фабричный метод стал пустым, можете сделать его абстрактным. Если в нём что-то осталось — не беда, это будет его реализацией по умолчанию.

Приведите пример паттерна Factory Method

Примером использования шаблона Factory Method может служить создание игры, где есть несколько видов оружия. Интерфейс Weapon определяет общий интерфейс для всех видов оружия, а классы-наследники определяют конкретные виды оружия, такие как пистолет, автомат, винтовка и т.д. Класс WeaponFactory является создателем оружия и определяет метод createWeapon(), который создает новый экземпляр оружия. Конкретные классы-фабрики реализуют метод createWeapon() для создания конкретного вида оружия.

Вот пример реализации шаблона Factory Method на языке Java:

// Интерфейс, определяющий общий интерфейс для всех видов оружия
public interface Weapon {
    public void fire();
}

// Конкретный класс, реализующий интерфейс Weapon
public class Pistol implements Weapon {
    public void fire() {
        System.out.println("Выстрел из пистолета");
    }
}

// Конкретный класс, реализующий интерфейс Weapon
public class MachineGun implements Weapon {
    public void fire() {
        System.out.println("Выстрел из автомата");
    }
}

// Абстрактный класс, определяющий метод createWeapon()
public abstract class WeaponFactory {
    public abstract Weapon createWeapon();
}

// Конкретная фабрика, создающая экземпляр пистолета
public class PistolFactory extends WeaponFactory {
    public Weapon createWeapon() {
        return new Pistol();
    }
}

// Конкретная фабрика, создающая экземпляр автомата
public class MachineGunFactory extends WeaponFactory {
    public Weapon createWeapon() {
        return new MachineGun();
    }
}

В этом примере интерфейс Weapon определяет общий интерфейс для всех видов оружия. Классы Pistol и MachineGun являются конкретными классами, реализующими этот интерфейс. Абстрактный класс WeaponFactory определяет метод createWeapon(), который создает новый экземпляр оружия. Конкретные классы-фабрики, такие как PistolFactory и MachineGunFactory, реализуют метод createWeapon() для создания конкретного вида оружия. Теперь, если нам нужно создать новый экземпляр оружия, мы можем использовать класс-фабрику, соответствующую нужному виду оружия, и вызвать ее метод createWeapon().

Пример использования шаблона Factory Method может выглядеть так:

// Создаем экземпляр фабрики для создания пистолета
WeaponFactory pistolFactory = new PistolFactory();

// Создаем новый экземпляр пистолета
Weapon pistol = pistolFactory.createWeapon();

// Выстреливаем из пистолета
pistol.fire();

Здесь мы создаем новый экземпляр PistolFactory, который является конкретной фабрикой для создания пистолета. Затем мы вызываем метод createWeapon(), чтобы создать новый экземпляр пистолета. Наконец, мы вызываем метод fire() для выстрела из пистолета.

Преимущества использования шаблона Factory Method:

  • Увеличение гибкости кода. Factory Method позволяет изменять код без изменения существующей логики.
  • Снижение зависимостей между объектами. Factory Method позволяет избежать жесткой связи между объектами, что делает код более модульным и легким для поддержки и развития.
  • Облегчение тестирования. Factory Method упрощает тестирование, так как мы можем использовать заглушки и моки для создания фабрик и объектов.
  • Расширяемость. Factory Method позволяет легко добавлять новые виды объектов, не затрагивая существующий код.

Расскажите о паттерне Proxy

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

Проблема Для чего вообще контролировать доступ к объектам? Рассмотрим такой пример: у вас есть внешний ресурсоёмкий объект, который нужен не все время, а изредка. Мы могли бы создавать этот объект не в самом начале программы, а только тогда, когда он кому-то реально понадобится. Каждый клиент объекта получил бы некий код отложенной инициализации. Но, вероятно, это привело бы к множественному дублированию кода. В идеале, этот код хотелось бы поместить прямо в служебный класс, но это не всегда возможно. Например, код класса может находиться в закрытой сторонней библиотеке.

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

Аналогия из жизни

image

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

image

Применимость

  • Ленивая инициализация (виртуальный прокси). Когда у вас есть тяжёлый объект, грузящий данные из файловой системы или базы данных. Вместо того, чтобы грузить данные сразу после старта программы, можно сэкономить ресурсы и создать объект тогда, когда он действительно понадобится.
  • Защита доступа (защищающий прокси). Когда в программе есть разные типы пользователей, и вам хочется защищать объект от неавторизованного доступа. Например, если ваши объекты — это важная часть операционной системы, а пользователи — сторонние программы (хорошие или вредоносные). Прокси может проверять доступ при каждом вызове и передавать выполнение служебному объекту, если доступ разрешён.
  • Локальный запуск сервиса (удалённый прокси). Когда настоящий сервисный объект находится на удалённом сервере. В этом случае заместитель транслирует запросы клиента в вызовы по сети в протоколе, понятном удалённому сервису.
  • Логирование запросов (логирующий прокси). Когда требуется хранить историю обращений к сервисному объекту. Заместитель может сохранять историю обращения клиента к сервисному объекту.
  • Кеширование объектов («умная» ссылка). Когда нужно кешировать результаты запросов клиентов и управлять их жизненным циклом. Заместитель может подсчитывать количество ссылок на сервисный объект, которые были отданы клиенту и остаются активными. Когда все ссылки освобождаются, можно будет освободить и сам сервисный объект (например, закрыть подключение к базе данных). Кроме того, Заместитель может отслеживать, не менял ли клиент сервисный объект. Это позволит использовать объекты повторно и здóрово экономить ресурсы, особенно если речь идёт о больших прожорливых сервисах.

Шаги реализации

  • Определите интерфейс, который бы сделал заместитель и оригинальный объект взаимозаменяемыми.
  • Создайте класс заместителя. Он должен содержать ссылку на сервисный объект. Чаще всего, сервисный объект создаётся самим заместителем. В редких случаях заместитель получает готовый сервисный объект от клиента через конструктор.
  • Реализуйте методы заместителя в зависимости от его предназначения. В большинстве случаев, проделав какую-то полезную работу, методы заместителя должны передать запрос сервисному объекту.
  • Подумайте о введении фабрики, которая решала бы, какой из объектов создавать — заместитель или реальный сервисный объект. Но, с другой стороны, эта логика может быть помещена в создающий метод самого заместителя.
  • Подумайте, не реализовать ли вам ленивую инициализацию сервисного объекта при первом обращении клиента к методам заместителя.

Приведите пример использования шаблона Proxy

Примером использования шаблона Proxy может служить создание загрузчика изображений. Оригинальный объект - это класс ImageLoader, который загружает изображения из сети. Заместитель - это класс ImageLoaderProxy, который загружает изображения с использованием ImageLoader и добавляет к ним дополнительную функциональность, например, кэширование результатов. Клиентский код работает с ImageLoaderProxy, а не с ImageLoader. Вот пример реализации шаблона Proxy на языке Java:

// Интерфейс, определяющий общий интерфейс для оригинального объекта и заместителя
public interface Image {
    public void display();
}

// Оригинальный класс, загружающий изображения из сети
public class ImageLoader implements Image {
    private String url;

    public ImageLoader(String url) {
        this.url = url;
        // загрузка изображения из сети
    }

    public void display() {
        System.out.println("Отображение изображения: " + url);
    }
}

// Класс-заместитель, загружающий изображения с помощью ImageLoader и добавляющий к ним новую функциональность
public class ImageLoaderProxy implements Image {
    private String url;
    private ImageLoader imageLoader;

    public ImageLoaderProxy(String url) {
        this.url = url;
    }

    public void display() {
        if (imageLoader == null) {
            imageLoader = new ImageLoader(url);
        }
        imageLoader.display();
    }
}

В этом примере интерфейс Image определяет общий интерфейс для оригинального объекта и заместителя. Класс ImageLoader является оригинальным объектом и реализует этот интерфейс. Класс ImageLoaderProxy является заместителем и также реализует интерфейс Image. Метод display() заместителя передает запрос на оригинальный объект ImageLoader, который загружает изображение из сети. Если изображение уже было загружено, заместитель просто отображает его, не загружая повторно. Пример использования шаблона Proxy может выглядеть так:

// Создаем экземпляр заместителя для загрузки изображения
Image image = new ImageLoaderProxy("https://example.com/image.jpg");

// Отображаем изображение
image.display();

Здесь мы создаем новый экземпляр ImageLoaderProxy, который является заместителем для загрузки изображения по URL-адресу https://example.com/image.jpg. Затем мы вызываем метод display() для отображения изображения. Если изображение еще не было загружено, заместитель ImageLoaderProxy загрузит его с помощью ImageLoader и добавит его в кэш. Если изображение уже было загружено, заместитель просто отобразит его из кэша, не загружая повторно.

Преимущества использования шаблона Proxy:

  • Управление доступом к оригинальному объекту. Заместитель позволяет контролировать доступ к оригинальному объекту и защищать его от неправильного использования.
  • Дополнительная функциональность. Заместитель может добавлять к оригинальному объекту новую функциональность, например, кэширование результатов, логирование и т.д.
  • Уменьшение нагрузки на сервер. Заместитель может кэшировать результаты запросов, что уменьшает нагрузку на сервер и ускоряет работу приложения.
  • Расширяемость. Заместитель может быть легко заменен на другой объект, не нарушая клиентский код.

Что такое «антипаттерн»? Какие антипаттерны вы знаете?

Антипаттерн (anti-pattern) — это распространённый подход к решению класса часто встречающихся проблем, являющийся неэффективным, рискованным или непродуктивным. Poltergeists (полтергейсты) - это классы с ограниченной ответственностью и ролью в системе, чьё единственное предназначение — передавать информацию в другие классы. Их эффективный жизненный цикл непродолжителен. Полтергейсты нарушают стройность архитектуры программного обеспечения, создавая избыточные (лишние) абстракции, они чрезмерно запутанны, сложны для понимания и трудны в сопровождении. Обычно такие классы задумываются как классы-контроллеры, которые существуют только для вызова методов других классов, зачастую в предопределенной последовательности.

Признаки появления и последствия антипаттерна

  • Избыточные межклассовые связи.
  • Временные ассоциации.
  • Классы без состояния (содержащие только методы и константы).
  • Временные объекты и классы (с непродолжительным временем жизни).
  • Классы с единственным методом, который предназначен только для создания или вызова других классов посредством временной ассоциации.
  • Классы с именами методов в стиле «управления», такие как startProcess.

Типичные причины

  • Отсутствие объектно-ориентированной архитектуры (архитектор не понимает объектно-ориентированной парадигмы).
  • Неправильный выбор пути решения задачи.
  • Предположения об архитектуре приложения на этапе анализа требований (до объектно-ориентированного анализа) могут также вести к проблемам на подобии этого антипаттерна.

Термины:

  • Внесенная сложность (Introduced complexity): Необязательная сложность дизайна. Вместо одного простого класса выстраивается целая иерархия интерфейсов и классов. Типичный пример «Интерфейс - Абстрактный класс - Единственный класс реализующий интерфейс на основе абстрактного».
  • Инверсия абстракции (Abstraction inversion): Сокрытие части функциональности от внешнего использования, в надежде на то, что никто не будет его использовать.
  • Неопределённая точка зрения (Ambiguous viewpoint): Представление модели без спецификации её точки рассмотрения.
  • Большой комок грязи (Big ball of mud): Система с нераспознаваемой структурой.
  • Божественный объект (God object): Концентрация слишком большого количества функций в одной части системы (классе).
  • Затычка на ввод данных (Input kludge): Забывчивость в спецификации и выполнении поддержки возможного неверного ввода.
  • Раздувание интерфейса (Interface bloat): Разработка интерфейса очень мощным и очень сложным для реализации.
  • Волшебная кнопка (Magic pushbutton): Выполнение результатов действий пользователя в виде неподходящего (недостаточно абстрактного) интерфейса. Например, написание прикладной логики в обработчиках нажатий на кнопку.
  • Перестыковка (Re-Coupling): Процесс внедрения ненужной зависимости.
  • Дымоход (Stovepipe System): Редко поддерживаемая сборка плохо связанных компонентов.
  • Состояние гонки (Race hazard): непредвидение возможности наступления событий в порядке, отличном от ожидаемого.
  • Членовредительство (Mutilation): Излишнее «затачивание» объекта под определенную очень узкую задачу таким образом, что он не способен будет работать с никакими иными, пусть и очень схожими задачами.
  • Сохранение или смерть (Save or die): Сохранение изменений лишь при завершении приложения.

к оглавлению

Что такое Dependency Injection?

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

к оглавлению

Источники

Вопросы для собеседования