ilavista
Все статьи

Вопросы на собеседовании - Общие

ООП, алгоритмы, паттерны проектирования, REST API, принципы S.O.L.I.D., Clean Code и Clean Architecture, и др. В этой статье мы рассмотрим некоторые из наиболее распространенных вопросов, с которыми сталкиваются программисты и предоставим ответы на них.

ООП

Что такое ООП?

Объектно-ориентированное программирование или ООП – это методология программирования, которая представляет программное обеспечение в виде набора объектов. Каждый объект – это не что иное, как экземпляр класса.

Разница между процедурным программированием и ООП

Процедурное программирование:

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

Объектно-ориентированное программирование:

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

Зачем использовать ООП?

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

Каковы основные концепции ООП?

Основными концепциями ООП являются:

  1. Наследование
  2. Инкапсуляция
  3. Полиморфизм
  4. Абстракция

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

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

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

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

Принципы проектирования (S.O.L.I.D.)

S.O.L.I.D. - это аббревиатура пяти основных принципов объектно ориентированной архитектуры. Эти принципы, собранные воедино, позволяют проще создавать, поддерживать и наследовать ПО.

S.O.L.I.D. состоит из:

  • S – Single-responsibility principle
  • O – Open-closed principle
  • L – Liskov substitution principle
  • I – Interface segregation principle
  • D – Dependency Inversion Principle

Принцип единственной ответственности — The Single Responsibility Principle

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

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

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

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

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

Принцип открытости/закрытости — The Open Closed Principle

Принцип декларирует, что программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения. Это означает, что эти сущности могут менять свое поведение без изменения их исходного кода.

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

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

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

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

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

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

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

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

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

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

Принцип разделения интерфейса — The Interface Segregation Principle

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

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

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

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

Принцип инверсии зависимостей — The Dependency Inversion Principle

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

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

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

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

REST API

Representational State Transfer (REST) в переводе — это передача состояния представления.

Если проще, то REST API — это когда серверное приложение дает доступ к своим данным клиентскому приложению по определенному URL.

Application Programming Interface (API), или программный интерфейс приложения — это набор инструментов, который позволяет одним программам работать с другими. API предусматривает, что программы могут работать в том числе и на разных компьютерах. REST API позволяет использовать для общения между программами протокол HTTP (зашифрованная версия — HTTPS), с помощью которого мы получаем и отправляем большую часть информации в интернете.

В API-системе четыре классических метода:

  1. GET — метод чтения информации. GET-запросы всегда только возвращают данные с сервера, и никогда их не меняют и не удаляют. В бухгалтерском приложении GET /invoices вы открываете список всех счетов.
  2. POST — создание новых записей. В нашем приложении POST /invoices используется, когда вы создаете новый счет на оплату.
  3. PUT — редактирование записей. Например, PUT /invoices вы исправляете номер счета, сумму или корректируете реквизиты.
  4. DELETE — удаление записей. В нашем приложении DELETE /invoices удаляет старые счета, которые контрагенты уже оплатили.

Таким образом, мы получаем четыре функции, которые одна программа может использовать при обращении к данным ресурса, в примере — это ресурс для работы со счетами /invoices. Построение API-системы с использованием ресурсов, HTTP и различных запросов к ним как раз и будет Representational State Transfer (REST API) — передачей состояния представления.

Архитектура REST API — самое популярное решение для организации взаимодействия между различными программами. Так произошло, поскольку HTTP-протокол реализован во всех языках программирования и всех операционных системах, в отличие от проприетарных протоколов.

Чаще всего REST API применяют:

  • Для связи мобильных приложений с серверными.
  • Для построения микросервисных серверных приложений. Это архитектурный подход, при котором большие приложения разбиваются на много маленьких частей.
  • Для предоставления доступа к программам сторонних разработчиков. Например, Stripe API позволяет программистам встраивать обработку платежей в свои приложения.