Solid принципы программирования: Простое объяснение принципов SOLID / Блог компании Mail.ru Group / Хабр

Содержание

Принципы SOLID в C#

112

IT блог — Принципы SOLID в C#

SOLID — это 5 принципов объектно-ориентированного программирования, описывающих архитектуру программного обеспечения. Вы можете заметить, что все шаблоны проектирования (паттерны) основаны на этих принципах. Аббревиатура SOLID расшифровывается как:

S (The Single Responsibility Principle) — принцип единой ответственности (SRP).

O (The Open Closed Principle) — обозначает принцип открытости/закрытости (OCP).

L (The Liskov Substitution Principle) – принцип подстановки Лисков, описывающий возможности заменяемости экземпляров объектов (LSP).

I (The Interface Segregation Principle) — принцип разделения интерйесов (ISP).

D (The Dependency Inversion Principle) — принцип инверсии зависимостей (DIP).

Я считаю, что с наглядными примерами SOLID, статья будет более доступной и понятной.

SRP – принцип единой ответственности

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

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

Давайте рассмотрим пример:

namespace SOLID
{
    public class Employee
    {
        public int ID { get; set; }
        public string FullName { get; set; }

        /// <summary>
        /// Данный метод добавляет в БД нового сотрудника
        /// </summary>
        /// <param name="em">Объект для вставки</param>
        /// <returns>Результат вставки новых данных</returns>
        public bool Add(Employee emp)
        {
            // Вставить данные сотрудника в таблицу БД
            return true;
        }

        /// <summary>
        /// Отчет по сотруднику
        /// </summary>
        public void GenerateReport(Employee em)
        {
            // Генерация отчета по деятельности сотрудника
        }
    }
}

В данном случае класс Employee не соответствует принципу SRP, т.к. несет две ответственности – добавление нового сотрудника в базу данных и создание отчета. Класс Employee не должен нести ответственность за отчетность, т.к. например, если через какое-то время вам скажут, что нужно предоставить отчет в формате Excel или изменить алгоритм создания отчета, вам потребуется отредактировать класс Employee.

Согласно SRP, один класс должен взять на себя ответственность, поэтому мы должны написать отдельный класс для генерации отчетов:

namespace SOLID
{
    public class Employee
    {
        public int ID { get; set; }
        public string FullName { get; set; }

        /// <summary>
        /// Данный метод добавляет в БД нового сотрудника
        /// </summary>
        /// <param name="em">Объект для вставки</param>
        /// <returns>Результат вставки новых данных</returns>
        public bool Add(Employee emp)
        {
            // Вставить данные сотрудника в таблицу БД
            return true;
        }
    }

    public class EmployeeReport
    {
        /// <summary>
        /// Отчет по сотруднику
        /// </summary>
        public void GenerateReport(Employee em)
        {
            // Генерация отчета по деятельности сотрудника
        }
    }
}

OCP — принцип открытости/закрытости

Главной концепцией данного принципа является то, что класс должен быть открыт для расширений, но закрыт от модификаций. Наш модуль должен быть разработан так, чтобы новая функциональность могла быть добавлена только при создании новых требований. «Закрыт для модификации» означает, что мы уже разработали класс, и он прошел модульное тестирование. Мы не должны менять его, пока не найдем ошибки. Как говорится, класс должен быть открытым только для расширений и в C# мы можем использовать для этого наследование.

Давайте рассмотрим наглядный пример:

public class EmployeeReport
{
    // <summary>
    /// Тип отчета
    /// </summary>
    public string TypeReport { get; set; }

    /// <summary>
    /// Отчет по сотруднику
    /// </summary>
    public void GenerateReport(Employee em)
    {
        if (TypeReport == "CSV")
        {
            // Генерация отче

SOLID принципы часть 1 — Еще один блог веб-разработчика

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

И так в этой статье я расскажу про пять архитектурных принципов программирования, которые помогут сделать ваш код гибким, понятным и легко поддерживаемым. Все о чем будет рассказано далее находится в книги Роберт К. Мартин Гибкая разработка программ. Принципы, примеры, практика (Agile Software Development Principles, Patterns and Practices )

Что такое Solid принципы?

Solid — переводится с английского как твердый, крепкий прочный, но с другой стороны SOLID это акроним, где каждая буква этого слова это отдельный принцип:

S — Single Responsible Principle (SRP)
O — Open Closed Principle (OCP)
L — Liskov Substitution Principle (LSP)

I — Interface Segregation Principle (ISP)
D — Dependency Inversion Principle (DIP)

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

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

В книги Роберта Мартина описано 7 признаков плохого кода:

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

И так начнем описание принципов.

Принцип персональной ответственности (SRP Single Responsibility Principle)

Каноническое определение принципа:

«Существует лишь одна причина, приводящая к изменению класса»

Том Де-Марко (1979) и Мейлер Пейдж-Джонсан (1988)

То есть этот принцип говорит, что каждый класс должен иметь лишь одну ответственность, то есть он должен отвечать только за одну вещь.

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

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

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

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

class Product {
  function get(name) {}
  function set(name, value) {}
  function save() {}
  function update() {}
  function delete() {}
  function show() {}
  function print() {}
}

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

class ProductEntity {
  function get(name) {}
  function set(name, value) {}
}

class ProductRepository {
  function save() {}
  function update() {}
  function delete() {}
}

class ProductView {
  function show() {}
  function print() {}
}

Как мы можем видеть код стал намного более понятным.

Рассмотрим еще один пример, класс User:

// сохранение данных user = new User() user.firstname = 'James' user.lastname = 'Bond' user.save() // получение данных из базы user2 = new User() user2.getByFirstName('John') // получение данных их связанной таблицы role = user2.role

При такой архитектуре все будет хорошо работать если проект будет небольшим. Но с ростом проекта увеличением количества требуемых методов, будет расти и сложность архитектуры, все очень быстро может стать запутанным, поэтому лучше это переделать. Разделить на сущность пользователя класс UserEntity и класс для работы с базой данных UserRepository

// сохранение данных
user = new UserEntity()
user.firstname = 'James'
user.lastname = 'Bond'
userRepository = new UserRepository()
userRepository.save(user)

// получение данных
userRepository2 = new UserRepository()
userRepository2.getByFirstName('John')

// получение данных их связанной таблицы
role = userRepository2.role

Еще один пример. Пусть у нас есть класс, который отвечает как за генерацию отчета и за его отправку определенному пользователю

 class FinancialReportMailer
  
  function FinancialReportMailer(transactions, account)
    this.transactions = transactions
    this.account = account
    this.report = ''

  function generate_report()
    // generate report
    this.report = make_report


  function send_report()
    // send report this.report


mailer = FinancialReportMailer(transactions, account)
mailer.generate_report()
mailer.send_report()

Класс FinancialReportMailer, показанный выше, выполняет две задачи: генерация отчета (метод «generate_report!») И отправка отчета (метод «send_report»). Класс выглядит довольно простым. Однако расширение этого класса в будущем может быть проблематичным, поскольку нам, вероятно, придется изменить логику класса. Принцип SRP говорит нам, что класс должен реализовывать одну единственную задачу, и поэтому в соответствии с этим принципом мы должны разделить класс FinancialReportMailer на два класса.

Давайте посмотрим, как выглядит этот код после того, как мы реорганизовали его для соответствия требованиям SRP:

class FinancialReportMailer
  function deliver(report, account)
    // send report
 

class FinancialReportGenerator
  function generate()
    // generate report
  

report = FinancialReportGenerator().generate()
FinancialReportMailer.deliver(report, account)

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

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

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

class Modem {
  function dial(){}
  function hangup() {}
  function send(request) {}
  function receive() {}
}

Если мы будем придерживаться принципа SRP, то нам нужно будет разделить этот класс на два класса DataChannel для организации соединения и Connection для передачи данных.

class DataChannel {
  function dial(){}
  function hangup() {}
}

class Connection {
  function send(request) {}
  function receive() {}
}

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

Суть принципа

На каждый объект должна быть возложена одна единственная обязанность

Принцип открытости / закрытости (OCP Open Closed Principle)

Каноническое определение принципа:

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

Бертран Майер (1988)

Как что-то может быть открытым и закрытым одновременно?!
Класс следует OCP, если он удовлетворяет этим двум критериям:

Открыт для расширения

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

Закрыт для модификации

При добавление нового функционала нельзя вносить изменения в исходный код такого класса. То есть весь новый код не должен затрагивать старый.

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

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

class Order {
  function getTotalPrice() {
    // получение всех позиций корзины
    // получение накопительности скидки текущего клиента
    // расчет итоговой цены
  }
}

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

class BaseOrderAlgorithm {
  function getTotalPrice() {
    products = this.getProducts()
    discount = this.getDiscount()
    return this.calculate(products, discount)
  }

  function getProducts() {}
  function getDiscount() {}
  function calculate(products, discount) {}
}

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

Первое решение для соответствия принципу OCP. Мы можем просто унаследовать дочерний класс MainOrderAlgorithm от BaseOrderAlgorithm и переопределить внутренние методы расчета getProducts и getDiscount. Таким образом мы сохраним старый код и добавим новый.

class MainOrderAlgorithm extends(BaseOrderAlgorithm) {
  function getProducts() {}
  function getDiscount() {}
}

Второе решение для соответствия принципу OCP.

class Order {
  function Order(IProduct product, IDiscount discount) {
    this.product = product
    this.discount = discount
  }
  function getTotalPrice() {
    products = this.product.getProducts()
    discount = this.discount.getDiscount()
    return this.calculate(products, discount)
  }
  function calculate(products, discount) {}
}

Через конструктор класса передаем отдельные классы product и discount И таким образом мы написали один раз класс Order и можем написать множество классов для обработки.

Применяйте принцип в местах подтвержденных частым изменениям.
Так же наличие условных операторов (if, case) при ветвление бизнес логики может сигнализировать о потенциальных точках изменения логики.

Возможная проблемы с принципом OCP

Пусть у нас есть некая фабрика которая на основе неких параметров создает новый класс

switch (name) {
  case 'Shape':
    return new Shape()
  case 'Rectangle':
    return new Rectangle()
}

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

Возможное решение

Полностью избавиться от подобных классов/методов почти невозможно поэтому:

  • Изолируйте такой код в классы фабрики
  • В таких ситуациях используйте внедрение зависимостей
  • Старайтесь зависеть от интерфейсов и абстракций а не от от конкретных классов (более подробно об этом позже)

Максимальное использование абстракций может показаться идеальным способом приведения любых классов в соответствие с принципом OCP. Но тут есть определенные проблемы о которых заранее лучше знать:

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

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

Пример с большим количеством if/case:

В приведенном ниже коде класс Logger форматирует и отправляет журналы логов. Но принцип OCP не соблюдается, поскольку нам придется изменять регистратор каждый раз, когда нам нужно добавить дополнительных типы отправлений или типы форматов:

class Logger
  function Logger(format, delivery)
    this.log(this.format(format), this.delivery(delivery))
  
  function log(string)
    this.deliver(this.format(string))

  function format(string):
    case:
      string == raw:
        // краткий формат
      string == with_date:
        // формат с датами
      string == with_date_and_details:
        // детализированный формат с датами
    else
      raise NotImplementedError
    

  function deliver(text):
    case:
      text == by_email:
        // отправка по почте
      text == by_sms:
        // отправка по смс
      text == to_stdout:
        // вывод в консоль
    else
      raise NotImplementedError
    

logger = Logger('raw', 'by_sms')
logger.log('Emergency error! Please fix me!')

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

class Logger
  function Logger(formatter, sender)
    this.formatter = formatter
    this.sender = sender

  function log(string)
    this.sender.deliver(this.formatter.format(string))


class LogSms
  function deliver(text):
    // отправка по СМС
    
class LogMailer
  function deliver(text):
    // отправка по почте
    
class LogWriter
  function deliver(log)
    // вывод в консоль
    
class DateFormatter
  function format(string)
    // формат с датой

class DateDetailsFormatter
  function format(string)
    // детализированный формат


class RawFormatter
  def format(string)
    // краткая форма формата

logger = Logger.new(RawFormatter(), LogSms())
logger.log('Emergency error! Please fix me!')

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

Далее в следующей статье я продолжу описание принципов.

Была ли вам полезна эта статья?

[6 / 4.8]


SOLID принципы. Рефакторинг — Блог программиста

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

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

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

Содержание:

  1. Рефакторинг
  2. Принципы чистого кода (SOLID)
    1. LSP — Liskov Substitution Principle
    2. DIP — Dependency Inversion Principle
    3. OCP — Open/Closed principle
    4. SRP — Single Responsibility Principle
    5. ISP — Interface Segregation Principle
  3. Заключение и литература по теме

В литературе достаточно подробно описаны методы рефакторинга [2, 3, 4], зачастую ими являются весьма нехитрые приемы [2], например:

  1. если функция не используется — удалите функцию;
  2. если название функции не отражает намерения программиста — переименуйте функцию.

Процессы написания нового кода и рефакторинга должны быть разделены — эту мысль замечательно выразил Кент Бек:

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

Рефакторинг должен проводиться постоянно, даже во время изучения кода. Однако он выделяется в качестве отдельного этапа TDD (Test Driven Development, разработки через тестирование) [5].

Даже у опытных программистов нередко бывает предчувствие, что в программе что-то не так, но не удается определить что именно. Фаулер замечательным образом систематизировал методы рефакторинга, а также отметил признаки плохого кода — так называемые «запахи» [2], однако более фундаментальными для объектно-ориентированного подхода являются принципы SOLID.

Аббревиатура SOLID была предложена Робертом Мартином и обозначает пять основных принципов объектно-ориентированного проектирования. Ссылки на многие из этих принципов можно встретить в книгах Кента Бека [4], Герба Саттера [6],  Скота Маерса [7], Мак-Колм Смита [8] и других исследователей.

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

LSP — Liskov Substitution Principle

Наследование является одним из базовых механизмов объектно-ориентированного программирования. Очень подробно этот механизм рассматривается Б. Мейером — он выделяет 12 видов наследования [10], однако, на практике чаще всего применяют наследование, реализующее отношения «является» (is-a, открытое наследование) или «реализуется посредством» (наследование реализации, закрытое наследование) [3, 6, 7]. Мак-Колм рассматривает наследование как базовый шаблон проектирования, при этом имеет ввиду is-a-наследование [8], именно к этому виду относится принцип LSP.

Принцип подстановки изначально сформулирован Барбарой Лисков и регламентирует правильное использование механизма наследования. Выделяются некоторый базовый тип, его подтип (класс-наследник). Согласно принципу LSP, программы должны быть написаны таким образом, чтобы в любом месте вместо базового типа мог быть подставлен подтип. Это означает, что классы наследники должны реализовывать интерфейс согласованно с интерфейсом базового класса.

В качестве примера рассмотрим классы геометрических фигур — точка, окружность, сфера.

Мы могли бы реализовать три класса, независимо друг от друга, но тогда каждый из них содержал бы данные с координатами и соответствующий набор функций — т.е. в нашей программе появился бы повторяющийся код. Согласно Фаулеру «дублирование кода свидетельствует об упущенной возможности для абстракции» [2], т.е. среди трех классов нам надо найти наиболее общий и применить механизм наследования, сделать это можно различными способами:

 

Liskov Substitution Principle example

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

  • добавление функции вычисления площади в класс окружности, будет означать появление такой же функции у сферы. Однако, у сферы нет площади;
  • добавление нового класса точки трехмерного пространства (Point3D). С одной стороны мы не можем создать такой класс на базе сферы, т.к. у Point3D не должно быть радиуса. С другой стороны при наследовании трехмерной точки от двумерной, мы получим дублирование кода в класса 3DPoint и Sphere. Устранить дублирование сложно потому, что теперь сфера должна наследовать трехмерную точку и окружность, однако они имеют общий базовый класс, а значит мы пришли к проблеме ромбовидного наследования.

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

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

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

Таким образом, принцип подстановки Лисков требует использования открытого наследования лишь для реализации отношения «является разновидностью». На примере показано, что мы не можем установить факт нарушения принципа LSP до тех пор, пока не узнаем как именно будут использоваться наши классы.

DIP — Dependency Inversion Principle

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

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

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

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

Strategy pattern example

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

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

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

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

  • не должно существовать прямых зависимостей между конкретными модулями. Модули должны зависеть лишь от абстракций;
  • абстракции не должны зависеть от деталей [3].

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

Dependency Inversion Principle example

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

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

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

OCP — Open/Closed principle

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

Открытым называется то, что можно менять. Закрытое изменять нельзя. Существуют различные подходы к понимаю того, каким образом класс должен закрываться — вплоть до требования распространять закрытые модули в скомпилированном виде, но обычно «закрытие» является чисто административным решением.

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

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

SRP — Single Responsibility Principle

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

Фаулер описывает принцип SRP через понятие зоны ответственности [2], под которой он имеет ввиду некоторый контекст в котором работает класс или отдельная функция. Внесения изменений в зону ответственности может повлечь необходимость изменения нашего класса. Класс каким либо образом изменяет свою зону ответственности — это является его обязанностью. Если принцип SRP не нарушается, то каждый класс имеет единственную обязанность, а значит — единственную зону ответственности и единственную причину для изменения. В связи с этим, Фаулер считает необходимость частого внесения изменений в класс «запахом» нарушения принципа единой обязанности.

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

ISP — Interface Segregation Principle

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

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

Interface Segregation Principle example

В верхней части диаграммы приведен класс автомобиля. Клиенты могут смотреть на него по-разному, например инспектор ГИБДД видит лишь номер двигателя и скорость, а жена водителя — бардачок и магнитолу. Мы не можем разделить класс на несколько частей, т.к. водителю нужно отслеживать текущую скорость (как инспектору) и хранить в бардачке документы (как это делает жена). Тем не менее, класс может выступать в разных ролях.

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

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

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

  1. Теория чистого кода. Стиль кодирования [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/1584. Дата обращения: 21.04.2015.
  2. Фаулер М., Бек К., Брант Д., Робертс Д., Апдайк У. Рефакторинг: улучшение существующего кода — Спб: Символ-Плюс, 2009. — 432 с
  3. Мартин Р. Чистый код. Создание, анализ и рефакторинг. Библиотека программиста. – СПб.: Питер, 2014. – 464 с.
  4. Beck K. Don’t Cross the Beams: Avoiding Interference Between Horizontal and Vertical Refactorings [Электронный ресурс] – режим доступа: https://www.facebook.com/notes/kent-beck/dont-cross-the-beams-avoiding-interference-between-horizontal-and-vertical-refac/260531380646400/. Дата обращения: 21.04.2015.
  5. Юнит-тестирование. Пример. Boost Unit Test [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/1549. Дата обращения: 21.04.2015.
  6. Герб Саттер. Решение сложных задач на C++. — М.: Издательский дом «Вильямс», 2002. — 400 с. — ISBN 5-8459-0352-1
  7. Майерс С. Эффективное использование C++. 50 рекомендаций по улучшению ваших программ и проектов. — СПб: Питер, 2006.
  8. Джейсон Мак-Колм Смит Элементарные шаблоны проектирования : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2013. — 304 с.
  9. Тепляков С. Liskov Substitution Principle [Электронный ресурс] – режим доступа: https://sergeyteplyakov.blogspot.com/2014/09/liskov-substitution-principle.html. Дата обращения: 21.04.2015.
  10. Мейер Б. Объектно-ориентированное конструирование программных систем. М.: Издательско-торговый дом «Русская Редакция», «Интернет-университет информационных технологий», 2005. 1232 с.: ил.
  11. Э. Гамма Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. – СПб.: Питер, 2009. – 366 с.

SOLID в объектно-ориентированном программировании – CODE BLOG

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

Что такое SOLID?

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

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

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

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

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

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

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

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

Сущности должны быть открыты для расширения, но закрыты для модификации.

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

The Liskov Substitution Principle (LSP) — Принцип подстановки Лисков

Наследующий класс должен дополнять, а не изменять базовый.

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

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

Клиенты не должны зависеть от методов, которые они не используют.

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

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

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

Под этой достаточно большой формулировкой имеется ввиду следующее (разберем каждое предложение по очереди):

  1. Должна быть выстроена четкая иерархия классов. Предки не должны зависеть от потомков.
  2. Связь между классами должна осуществляться через интерфейсы, а не напрямую.
  3. В интерфейсах и абстрактных классах не должно быть конкретных реализаций кода, только сигнатура
  4. Конкретные классы должны реализовывать интерфейсы

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

Рекомендую ознакомиться с отдельной статье посвященной этой теме Инверсия управления и Внедрение зависимостей (IoС & DI)

Заключение

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

Примеры использования данного принципа в языке программирования C# вы сможете найти в статье Принципы SOLID C#. А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.

Похожее

Принципы SOLID в картинках / Блог компании Цифровые Экосистемы / Хабр

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

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

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

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

Ну, приступим.

Принципы SOLID

S – Single Responsibility (Принцип единственной ответственности)

Каждый класс должен отвечать только за одну операцию.

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

Назначение

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

O — Open-Closed (Принцип открытости-закрытости)

Классы должны  быть  открыты для расширения, но закрыты для модификации.

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

Назначение

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

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

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


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

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

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

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

Назначение

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

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

Не следует ставить клиент в зависимость от методов, которые он не использует.

Когда классу приходится производить действия, не несущие никакой реальной пользы, это выливается в пустую трату ресурса, а в случае, если класс выполнять эти действия не способен, ведёт к возникновению багов.

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

Назначение

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

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

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


Для начала объясню термины, которые здесь применяются, простыми словами.

Модули (или классы) верхнего уровня = классы, которые выполняют операцию при помощи инструмента
Модули (или классы) нижнего уровня = инструменты, которые нужны для выполнения операций
Абстракции – представляют интерфейс, соединяющий два класса
Детали = специфические характеристики работы инструмента

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

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

Назначение

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

Обобщая сказанное

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

SOLID / Хабр

SOLID критикует тот, кто думает, что действительно понимает ООП
© Куряшкин Виктор

Я знаком с принципами SOLID уже 6 лет, но только в последний год осознал, что они означают. В этой статье я дам простое объяснение этим принципам. Расскажу о минимальных требованиях к языку программирования для их реализации. Дам ссылки на материалы, которые помогли мне разобраться.


Первоисточники

Придумал принципы SOLID Роберт Мартин (Uncle Bob). Естественно, что в своих работах он освещает эту тему.

Книга “Принципы, паттерны и методики гибкой разработки на языке C#” 2011 года. Большинство статей, которые я видел, основываются именно на этой книге. К сожалению, она дает расплывчатое описание принципов, что сильно ударило по их популярности.

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

Книга “Clean Architecture” 2017 года. Описывает архитектуру, построенную из кирпичиков, удовлетворяющих SOLID принципам. Дает определение структурному, объектно-ориентированному, функциональному программированию. Содержит лучшее описание SOLID принципов, которое я когда-либо видел.


Требования

SOLID всегда упоминают в контексте ООП. Так получилось, что именно в ООП языках появилась удобная и безопасная поддержка динамического полиморфизма. Фактически, в контексте SOLID под ООП понимается именно динамический полиморфизм.

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


  • Динамический полиморфизм — это про абстрактные классы, интерфейсы, утиную типизацию, т.е. только в рантайме будет понятно, с каким типом будет работать наш код.
  • Статический полиморфизм — это в основном про шаблоны (genererics). Когда уже на этапе компиляции из одного шаблонного кода генерируется код специфичный для каждого используемого типа.

Кроме привычных языков вроде Java, C#, Ruby, JavaScript, динамический полиморфизм реализован, например в


  • Golang, с помощью интерфейсов
  • Clojure, с помощью протоколов и мультиметодов
  • в прочих, совсем не “ООП” языках

Принципы

SOLID принципы советуют, как проектировать модули, т.е. кирпичики, из которых строится приложение. Цель принципов — проектировать модули, которые:


  • способствуют изменениям
  • легко понимаемы
  • повторно используемы

SRP: The Single Responsibility Principle

A module should be responsible to one, and only one, actor.
Старая формулировка: A module should have one, and only one, reason to change.

Часто ее трактовали следующим образом: Модуль должен иметь только одну обязанность. И это главное заблуждение при знакомстве с принципами. Все несколько хитрее.

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


OCP: The Open Closed Principle

A software artifact should be open for extension but closed for modification.
Старая формулировка: You should be able to extend a classes behavior, without modifying it.

Это определенно может ввести в ступор. Как можно расширить поведение класса без его модификации? В текущей формулировке Роберт Мартин оперирует понятием артефакт, т.е. jar, dll, gem, npm package. Чтобы расширить поведение, нужно воспользоваться динамическим полиморфизмом.

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

Этот принцип тесно связан с LSP и DIP, которые мы рассмотрим далее.


LSP: The Liskov Substitution Principle

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

Классический пример нарушения. Есть базовый класс Stack, реализующий следующий интерфейс: length, push, pop. И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.

У этого принципа есть забавное следствие: Объекты, моделирующие сущности, не обязаны реализовывать отношения этих сущностей. Например, у нас есть целые и вещественные числа, причем целые числа — подмножество вещественных. Однако, double состоит из двух int: мантисы и экспоненты. Если бы int наследовал от double, то получилась бы забавная картина: родитель содержит 2-х своих детей.

В качестве второго примера можно привести Generics. Допустим, есть базовый класс Shape и его потомки Circle и Rectangle. И есть некая функция Foo(List<Shape> list). Мы считаем, что List<Circle> можно привести к List<Shape>. Однако, это не так. Допустим, это приведение возможно, но тогда в list можно добавить любую фигуру, например rectangle. А изначально list должен содержать только объекты класса Circle.


ISP: The Interface Segregation Principle

Make fine grained interfaces that are client specific.

Под интерфейсом здесь понимается именно Java, C# интерфейс. Разделение интерфейса облегчает использование и тестирование модулей.


DIP: The Dependency Inversion Principle

Depend on abstractions, not on concretions.


  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Что такое модули верхних уровней? Как определить этот уровень? Как оказалось, все очень просто. Чем ближе модуль к вводу/выводу, тем ниже уровень модуля. Т.е. модули, работающие с BD, интерфейсом пользователя, низкого уровня. А модули, реализующие бизнес-логику — высокого уровня.

Что такое зависимость модулей? Это ссылка на модуль в исходном коде, т.е. import, require и т.п. С помощью динамического полиморфизма в runtime можно обратить эту зависимость.

Есть модуль Logic, реализующий логику, который должен отсылать уведомления. В этом же пакете объявляется интерфейс ISender, который используется Logic. Уровнем ниже, в другом пакете объявляется ConcreteSender, реализующий ISender. Получается, что в момент компиляции Logic не зависит от ConcreteSender. В runtime, например, через конструктор в Logic устанавливается экземпляр ConcreteSender.

Отдельно стоит отметить частый вопрос “Зачем плодить абстракции, если мы не собираемся заменять базу данных?”.

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

Принципы SOLID подходят для проектов, разрабатываемых по гибким методологиям, ведь Роберт Мартин — один из авторов Agile Manifesto.

Принципы SOLID стремятся свести изменение модулей к их добавлению и удалению.

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

Принцип SOLID в языке Go / Хабр

Приветствую вас, хабровчане, решил поделиться с сообществом переводом довольно часто (по личным наблюдениям) упоминаемого поста SOLID Go Design из блога Dave Cheney, который выполнял для собственных нужд, но кто-то говорил, что нужно делиться. Возможно для кого-то это окажется полезным.


Этот пост на основе текста из основного доклада GolangUK прошедшего 18-ого Августа 2016.
Запись выступления доступна в YouTube.


Как много программистов на Go в мире?

Как много программистов на Go в мире? Подумайте о числе и держите его в своей голове,
мы вернемся к этому вопросу в конце разговора.


Рецензирование кода

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

Если рецензирование кода нужно для того, чтобы отловить плохой код, тогда как вы узнаете, что код, который вы рецензируете, хороший или плохой?

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


Плохой код

Какими могут быть свойства плохого кода, которые вы можете использовать при рецензировании?


  • Негибкий. Является ли код негибким? Содержит ли он жесткий набор типов и параметров, что затрудняет модификацию.
  • Хрупкий. Является ли код хрупким? Вызывает ли хаос малейшее изменение кодовой базы?
  • Неподвижный. Сложно ли код поддается рефакторингу? Находится ли он в одном нажатии клавиши от цикличного импорта?
  • Cложный. Существует ли этот код только ради кода и не является ли он переусложненным?
  • Многословный. Использование кода изнурительно? Можете ли вы глядя на код сказать, что он пытается сделать?

Относятся ли эти слова к позитивным? Доставило бы вам удовольствие слышать эти слова при рецензировании вашего кода?

Возможно, что нет.


Хороший дизайн

Но это улучшение, теперь мы можем сказать что то вроде «мне не нравится это потому, что слишком сложно модифицировать», или «мне не нравится это потому, что я не могу сказать, что этот код пытается сделать», но что насчет того, чтобы вести обсуждение позитивно?

Разве это не было бы здорово, если бы существовал способ описать свойства хорошего дизайна, а не только плохого и иметь возможность рассуждать объективными терминами?


SOLID

В 2002 году Роберт Мартин опубликовал свою книгу Agile Software Development, Principles, Patterns, and Practices. В ней он описал пять принципов переиспользуемого дизайна програмного обеспечения, которые он назвал SOLID принципами, аббревиатурой их названий.


  • Принцип единственной ответственности
  • Принцип открытости/закрытости
  • Принцип подстановки Барбары Лисков
  • Принцип разделения интерфейса
  • Принцип инверсии зависимостей

Эта книга слегка устарела, языки о которых ведется разговор использовались порядка 10 лет назад. Но, возможно, существуют некоторые аспекты SOLID принципа, которые могут предоставить нам ключ к разгадке того, как говорить о хорошо разработанных программах на Go.

Это именно то, что я хотел бы обсудить с вами этим утром.


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

Первый принцип SOLID, это S — принцип единой ответственности.


Класс должен иметь одну и только одну причину для изменений.
-Роберт С. Мартин

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

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

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


Связанность & Единство

Два слова, которые описывают на сколько просто вносить изменения в вашу программу, это связанность и единство.

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

Смежное, но отдельное понятие, это единство — сила взаимного притяжения.

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

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


Имена пакетов

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


  • net/http, который предоставляет http клиент и сервер.
  • os/exec, который запускает внешние команды.
  • encoding/json, который реализует кодирование и декодирование документов JSON.

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


Плохие имена пакетов

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

Какую возможность предоставляет package server?.. возможно это сервер, но с какой протокол он реализует?

Какую возможность предоставляет package private? Штуки, которые я не должен увидеть? Должен ли он вообще иметь какие -то публичные символы?

И package common, ровно как и его соучастник package utils часто находятся рядом с другими злостными нарушителями.

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


Филисофия UNIX в Go

В моем представлении, никакое обсуждение раздельного дизайна не будет полным без упоминания труда Дугласа Маклроя «Филисофия UNIX»; мелкие, острые инструменты, которые сочетаются для решения более крупных задач, часто таких, которые не были предусмотренны оригинальными авторами. Я думаю что пакеты Go воплощают дух философии UNIX. В действительности каждый пакет Go сам по себе -это маленькая Go программа, единственная точка изменений с единственной ответственностью.


Принцип открытости/закрытости

Второй принцип O — принцип открытости / закрытости Бертрана Мейера, который в 1988 году писал:


Програмные объекты должны быть открыты для расширения и закрыты для модификации.
-Бертран Мейер, Построение Объектно-Ориентированного Программного Обеспечения

Как этот совет применялся для языков созданных 21 год назад?

package main

type A struct {
        year int
}

func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) }

type B struct {
        A
}

func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) }

func main() {
        var a A
        a.year = 2016
        var b B
        b.year = 2016
        a.Greet() // Hello GolangUK 2016
        b.Greet() // Welcome to GolangUK 2016
}

У нас есть тип A, с полем year и методом Greet. Мы имеем второй тип B в который встроен A, вызовы методов B перекрывают вызовы методов A поскольку A встроен, как поле в B и B предлогает свой собственный метод Greet скрывая аналогичный в A.

Но встраивание существует не только для методов, оно также предоставляет доступ к встроенным полям типа. Как вы можете увидеть, поскольку оба A и B определены в одном пакете, B может получить доступ к приватному полю year в A, как будто оно было определенно внутри B.

Итак встраивание-это мощный инструмент, который позволяет типам в Go быть открытыми для расширения.

package main

type Cat struct {
        Name string
}

func (c Cat) Legs() int { return 4 }

func (c Cat) PrintLegs() {
        fmt.Printf("I have %d legs\n", c.Legs())
}

type OctoCat struct {
        Cat
}

func (o OctoCat) Legs() int { return 5 }

func main() {
        var octo OctoCat
        fmt.Println(octo.Legs()) // 5
        octo.PrintLegs()         // I have 4 legs
}

В этом примере у нас есть тип Cat, который может посчитать колличество ног с помощью своего метода Legs. Мы встраиваем этот тип Cat в новый тип OctoCat и декларируем то, что OctocatS имеет пять ног. При этом OctoCat определяет свой собственный метод Legs, который возвращается 5, когда вызывается метод PrintLegs, он возвращает 4.

Это происходит потому, что PrintLegs определен внутри типа Cat. Он принимает Cat в качестве ресивера и отсылает к методу Legs типа Cat. Cat должен знать о типе в который он был встроен, поэтому его метод не может быть изменен встраиванием.

Отсюда мы можем сказать, что типы в Go открыты для расширения и закрыты для модификации.

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

func (c Cat) PrintLegs() {
        fmt.Printf("I have %d legs\n", c.Legs())
}

func PrintLegs(c Cat) {
        fmt.Printf("I have %d legs\n", c.Legs())
}

Ресивер-это в точности то, что вы передаете в него, первый параметр функции и посколько Go не поддерживает перегрузку функций, OctoCat не является взаимозаменяемым с обычным типом Cats. Что подводит меня к следующему принципу.


Принцип подстановки Барбары Лисков

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

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


Интерфейсы

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

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

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


io.Reader

type Reader interface {
        // Read reads up to len(buf) bytes into buf.
        Read(buf []byte) (n int, err error)
}

Что приводит меня к io.Reader моему любимому интерфейсу в Go.

Интерфейс io.Reader очень прост; Read читает данные в указанный буфер и возвращает вызывающему коду число байт, которые были прочитаны, и любую ошибку, которая может возникнуть в процесе чтения. Это выглядит просто, но это очень мощно.

Поскольку io.Reader имеет дело с чем угодно, что может быть выраженно как поток байт, мы может конструировать объекты читатели буквально из чего угодно; константной строки, массива байт, стандартного потока входа, сетевого потока, архива gzip tar, стандартного выходного потока или команды выполненной удаленно через ssh.

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

Итак, принцип подстановки Лисков применим в Go и сказанное можно суммировать прекрасным афоризмом покойного Джима Вейриха:


Требуй не больше, обещай не меньше.
-Джим Вейрих

И это отличный переход к четвертому принципу SOLID.


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

Четвертый принцип, это принцип разделения интерфейса, который читается, как:


Клиенты не должны быть вынужденны зависеть от методов, которые они не используют.
-Роберт С. Мартин

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

// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error

Я могу определить такую функцию. давайте назовем ее Save, она принимает *os.File как источник для записи предоставленного Document. Но здесь возникает несколько проблем.

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

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

*os.File также определяет много методов, которые не релевантны Save, как чтение дирректорий и проверка того, является ли путь символической ссылкой. Было бы полезно, если бы сигнатура нашей функции Save была описанна только теми частями *os.File, которые релевантны ее задаче.

Что мы можем сделать с этими проблемами?

// Save writes the contents of doc to the supplied ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error

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

С этими изменениями любой тип, который реализует интерфейс io.ReadWriteCloser может быть замещен предыдущим *os.File. Это делает применение Save более широким и поясняет стороне вызывающей Save, какой метод из типа *os.File релевантный требуемой операции.

Как автор Save я более не должен иметь возможность вызова всех нерелевантных методов из *os.File, поскольку они спрятанны за io.ReadWriteCloser интерфейсом. Но мы можем пойти немного дальше с методом разделения интерфейса.

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

// Save writes the contents of doc to the supplied WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error

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

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

type NopCloser struct {
        io.Writer
}

// Close has no effect on the underlying writer.
func (c *NopCloser) Close() error { return nil }

Грубым решением будет определение нового типа, который встраивает io.Writer и переопределяет метод Close, предотвращая вызов Save из закрытого основного потока.

Но это будет скорее всего нарушением принципа подстановки Барбары Лисков, поскольку NopCloser на самом деле ничего не закрывает.

// Save writes the contents of doc to the supplied Writer.
func Save(w io.Writer, doc *Document) error

Куда лучше будет решение переопределить Save, принимая только io.Writer, полностью лишив его возможности делать что -то помимо записи данных в поток.

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


Важное эмпирическое правило для Go, принимать интерфейсы, а возвращать структуры.
-Джек Линдамуд

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

В этой версии в рамках стандартного твита не хватает одного нюанса и это не вина Джека, но я думаю, что она представляет одну из главных причин появления приданий о дизайне языка Go.


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

Последний принцип SOLID, это принцип инверсии зависимостей, который утверждает:


Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба уровня должны зависеть от абстракций.
Абстракции не должны зависеть от их деталей. Детали должны зависеть от абстракций.
-Роберт С. Мартин

Но что означает инверсия зависмостей на практике для программиста на Go?

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

Итак, как я представляю себе то, о чем Мартин тут говорит, в основном в контексте Go-это структура вашего графа импортов.

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

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


SOLID дизайн Go

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

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

Принцип открытости / закрытости поощряет вас к компромису простых типов и более сложных путем использования встривания.

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

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

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

Если вы хотите подвести итог этого разговора, то скорее всего это будет: интерфейсы позволяют вам применять принципы SOLID в Go программах.

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

Как заметил Санди Метз:


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

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


Заключение

В завершении давайте вернемся к вопросу, которым я открыл этот разговор. Как много программистов на Go во всем мире? Вот мое предположение:


В 2020 году будет 500,000 разработчиков на Go.
-Дейв Чейни

Что половина миллиона программистов на Go будут делать со своим временем? Чтож, очевидно, они будут писать много кода на Go и если мы будем честны, не весь код будет хорошим, часть кода будет плохим.

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


В С++ есть гораздо более чистый и эволюционный язык, который пытается выйти.
-Бьёрн Страуструп, Дизайн и Эволюция С++

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

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

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

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

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


… и еще одна деталь

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

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

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

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

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

Спасибо.



Original post by Dave Cheney

Принципы

SOLID: объяснения и примеры | by Simon LH

SOLID — это сокращение от 5 важных принципов проектирования при выполнении ООП (объектно-ориентированного программирования).

Эти 5 принципов были представлены Робертом К. Мартином (дядя Боб) в его статье 2000 года Принципы проектирования и шаблоны проектирования .
Настоящая аббревиатура SOLID была, однако, идентифицирована позже Майклом Фезерсом.

Цель этих принципов — сделать проекты программного обеспечения более понятными, более простыми в обслуживании и расширении.
Инженер-программист должен знать эти 5 принципов!

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

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

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

Давайте рассмотрим пример того, как написать фрагмент кода, нарушающий этот принцип.

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

Попробуем исправить.

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

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

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

Это может показаться запутанным, поэтому давайте рассмотрим пример, который очень ясно прояснит, что я имею в виду.

В этом фрагменте кода нам нужно делать что-то конкретное, когда сообщение начинается с символа «#».
Однако вышеупомянутая реализация нарушает принцип открытого / закрытого типа, так как этот код отличается от поведения начальной буквы.
Если позже мы захотим также включить упоминания, начинающиеся с «@», нам пришлось бы изменить класс с дополнительным «else if» в методе CreatePost () .

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

Используя наследование, теперь намного проще создать расширенное поведение для объекта Post, переопределив метод CreatePost () .
Оценка первого символа ‘#’ теперь будет обрабатываться где-нибудь в другом месте (возможно, на более высоком уровне) нашего программного обеспечения, и самое интересное то, что если мы хотим изменить способ оценки postMessage, мы можем изменить код там, не затрагивая ни одну из этих основных частей поведения.

Этот, вероятно, самый сложный, чтобы осознать, когда его вводят впервые.

В программировании принцип замены Лискова гласит, что если S является подтипом T , то объекты типа T могут быть заменены (или заменены) объектами типа S .
Математически это можно сформулировать как

Пусть ϕ (x) будет доказуемым свойством для объектов x типа T .
Тогда
ϕ (y) должно быть истинным для объектов y типа S , где S — подтип .

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

Давайте посмотрим на пример того, как нарушить этот принцип.

Обратите внимание, как вызов CreatePost () в случае подтипа MentionPost не будет делать то, что должен делать; уведомить пользователя и переопределить существующее упоминание.
Поскольку метод CreatePost () не переопределяется в MentionPost , вызов CreatePost () будет просто делегирован вверх по иерархии классов и вызовет CreatePost () из его родительского класса.

Давайте исправим это

Реорганизуя класс MentionPost таким образом, что мы переопределяем метод CreatePost () , а не вызываем его для его базового класса, мы больше не нарушаем принцип подстановки Лискова.

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

Этот принцип довольно легко понять.
На самом деле, если вы привыкли использовать интерфейсы, скорее всего, вы уже применяете этот принцип.
Если нет, пора начинать!

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

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

В этом примере представим, что у меня сначала есть интерфейс IPost с подписью метода CreatePost () .
Позже я модифицирую этот интерфейс, добавляя новый метод ReadPost () , чтобы он стал похож на интерфейс IPostNew .

Здесь мы нарушаем принцип разделения интерфейса.
Вместо этого просто создайте новый интерфейс.

Если какому-либо классу может потребоваться как метод CreatePost () , так и метод ReadPost () , он реализует оба интерфейса.

Наконец, мы подошли к D, последнему из 5 принципов.

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

  • модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

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

Давайте посмотрим на пример.

Посмотрите, как мы создаем экземпляр ErrorLogger из класса Post .
Это нарушение принципа инверсии зависимостей.
Если бы мы хотели использовать другой тип регистратора, нам пришлось бы изменить класс Post.

Давайте исправим это с помощью внедрения зависимостей.

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

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

Вот и все!
Если у вас есть какие-либо вопросы или отзывы, пожалуйста, оставьте комментарий ниже.

.Объяснение

SOLID принципов объектно-ориентированного программирования | Apiumhub

Сегодня я хотел бы поговорить о S.O.L.I.D., первых пяти принципах объектно-ориентированного программирования, которые мы в Apiumhub (как и многие другие) считаем необходимыми для создания работающего программного обеспечения. Если вы не знали, в компьютерном программировании акроним SOLID rules был введен Майклом Фезерсом для пяти принципов, определенных Робертом К. Мартином в начале 2000-х годов.

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

Хорошо, давайте начнем с основ, S.O.L.I.D. обозначает:

S — Принцип единоличной ответственности

O — Принцип открыт-закрыт

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

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

Зависимость Принцип инверсии

Давайте рассмотрим каждый принцип отдельно, чтобы понять, почему S.O.L.I.D может помочь разработчикам создавать качественное программное обеспечение.

Принципы SOLID

1. Принцип единственной ответственности

«Не должно быть более одной причины для изменения класса».

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

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

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

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

2. Принцип «открыто-закрыто»

«Программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации.”

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

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

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

«подтипы должны быть заменяемыми для своих базовых типов»

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

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

Многие говорят, что принцип замещения Лискова очень тесно связан с предыдущим принципом, принципом открытого-закрытого. Роберт С. Мартин даже говорит, что «нарушение LSP — это скрытое нарушение OCP».

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

«Классы, реализующие интерфейсы, не должны принудительно реализовывать методы, которые они не используют».

Здесь речь идет о написании интерфейсов.Итак, что заявлено? По сути, когда интерфейс становится слишком большим / толстым, нам абсолютно необходимо разделить его на небольшие интерфейсы, которые более конкретны. И интерфейс будет определяться клиентом, который будет его использовать, что означает, что клиент интерфейса будет знать только о методах, которые связаны с ним.

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

5. Принцип инверсии зависимостей

«Модули высокого уровня не должны зависеть от модулей низкого уровня, а оба должны зависеть от абстракции. Абстракция не должна зависеть от деталей; скорее, детали должны зависеть от абстракции ».

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

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

Заключение

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

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

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

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

Если вам это понравилось, вам может понравиться…

.

Обучение S.O.L.I.D. Принципы программирования — Обзор принципов SOLID

Детали курса

Есть много принципов, поддерживающих хороший объектно-ориентированный дизайн и программирование. SOLID описывает пять из этих принципов: единственная ответственность, открытый-закрытый, подстановка Лискова, разделение интерфейса и инверсия зависимостей. Эти принципы определяют создание стабильного, масштабируемого и надежного кода.

Чтобы включить SOLID в свой рабочий процесс разработки, Стивен Лотт подготовил серию уроков, в которых один за другим разбираются принципы с примерами из реальной жизни.Узнайте, как использовать эти принципы в процессе проектирования, и по ходу проверять надежность вашего кода. Стивен использует Python для демонстрации концепций, но они полезны для любого объектно-ориентированного языка программирования.

Инструктор

  • Click here to view Steven Lott’s instructor page

    Стивен Лотт

    Стивен Ф.Лотт — контрактный разработчик программного обеспечения и архитектор.

    Стивен занимается программированием с 70-х годов, когда компьютеры были большими, дорогими и редкими. Он использует Python для решения бизнес-задач более 10 лет, а в настоящее время использует Python для реализации микросервисов и конвейеров ETL.

    Стивен — самопровозглашенный техномад, живущий в разных местах на восточном побережье США, работая консультантом, автором и разработчиком. Его технический блог — http: // slott-softwarearchitect.blogspot.com.

    Узнать больше Видеть меньше

Навыки, описанные в этом курсе

Зрители этого курса

27 759 человек смотрели этот курс

Что они делают

Разработчик программного обеспечения

Связанные курсы

.

SOLID для функционального программирования — Stack overflow на русском

Переполнение стека
  1. Около
  2. Продукты
  3. Для команд
  1. Переполнение стека Общественные вопросы и ответы
  2. Переполнение стека для команд Где разработчики и технологи делятся частными знаниями с коллегами
  3. Вакансии Программирование и связанные с ним технические возможности карьерного роста
  4. Талант Нанимайте технических специалистов и создавайте свой бренд работодателя
  5. Реклама Обратитесь к разработчикам и технологам со всего мира
  6. О компании
.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Theme: Overlay by Kaira Extra Text
Cape Town, South Africa