Назад к основам | Напишите лучший код

«Хороший дизайн увеличивает ценность быстрее, чем увеличивает стоимость».

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

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

S в SOLID означает принцип единой ответственности, в нем говорится:

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

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

class Dog
  def initialize(name = "doggo")
     @name = name
  end
  def walk
    puts "I walk"
  end
  def bark
    puts "woof woof"
  end
  
  def eat
    puts "I eat chimken"
  end
  
  def tail(attribute = "pretty") 
    puts "I have a #{attribute} tail"
  end
end
Now, if we want to add another method bite(), it would be super easy to add because our class Dog has just one responsibility that is to behave like a Dog. If we want to add a method fly we need to create another class, because fly is the behavior of a bird.
class Bird
  def initialize(name = "birdie")
    @name = name
  end
   def fly
     puts "I fly"
   end
end

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

Переходя ко второму принципу, принципу открытости / закрытости, мы говорим:

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

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

Мы можем использовать наследование / модули для расширения классов и добавления к ним поведения, или мы можем спроектировать классы таким образом, чтобы части логики могли быть подключены без суеты. Давайте возьмем реальный пример: мы создали общий класс Dog, но, как мы знаем, разные породы ведут себя по-разному. Теперь, что, если я хочу добавить поведение двух разных видов собак, одна из которых - золотистый ретривер, а другая - корги. Что я должен делать? Должен ли я создать два отдельных класса? Что мы можем сделать, так это унаследовать от класса Dog, а затем при необходимости расширить поведение.

class GoldenRetriver < Dog
  def bark
    "I don't bork unnecessarily"
  end 
  
  def characteristics
    "I am large in size and intelligent and playful"  
  end
  
  def retrieve(things = "toys")
    "I retrieve #{things}"
  end
end
class Corgi < Dog
  def bark
    "I bork pretty lot"
  end
  
  def characteristics
    "I am small and Cheerful and lively"
  end
  
  def herd(thing = "sheep")
    "I herd #{thing}"
  end
end
Here, both classes are derived from Dog and would behave as a Dog albeit with additional behaviors. Hence the Dog class is closed for modification(We didn't change any methods of the Dog class), and is open for modification(We extended Dog class via inheritance).

Третий принцип, Принцип замещения Лискова, гласит:

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

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

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

class Dog 
  def tail(attribute = "pretty") 
    puts "I have a #{attribute} tail"
  end
end
class Corgi < Dog
  def tail 
    raise "I don't have a tail"
  end
end
Note: You should be able to use subtypes of class interchangeably, with the instance of parent class, this will allow you to easily swap out different implementations without affecting existing code.
Also, if you have to check instance_of? or something similar before calling a method, it's an indication that you are violating the Liskov substitution.

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

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

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

Интернет-провайдер разделяет очень большие интерфейсы на более мелкие и более конкретные, так что клиентам нужно будет знать только о методах, которые их интересуют. Такие сжатые интерфейсы также называются ролевыми интерфейсами. ISP предназначен для того, чтобы система оставалась изолированной, и, таким образом, ее было легче реорганизовать, изменить и повторно развернуть. Этот принцип на самом деле неприменим к Ruby, который для определения пригодности полагается на методы и свойства объектов, а не только на их тип. Здесь нет примера кода, извините.

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

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

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

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