Василий Половнёв

Разработчик, зануда, сооснователь Kursk Meetup.

Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.

Связаться: vasily@polovnyov.ru или твитер.

Ушпешный блог Канал в Телеграме Тестовый курс

Конспект POODR. Reducing Costs with Duck Typing

Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.

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

Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.

Утиная типизация

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

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

Users of an object need not, and should not, be concerned about its class. Class is just one way for an object to acquire a public interface; the public interface an object obtains by way of its class may be one of several that it contains. Applications may define many public interfaces that are not related to one specific class; these interfaces cut across class. Users of any object can blithely expect it to act like any, or all, of the public interfaces it implements. It’s not what an object is that matters, it’s what it does.

If your design imagination is constrained by class and you find yourself unexpectedly dealing with objects that don’t understand the message you are sending, your tendency is to go hunt for messages that these new objects do understand.

Про абстрактное и конкретное:

The concreteness of the first example makes it simple to understand but dangerous to extend. The final, duck typed, alternative is more abstract; it places slightly greater demands on your understanding but in return offers ease of extension. Now that you have discovered the duck, you can elicit new behavior from your application without changing any existing code; you simply turn another object into a Preparer and pass it into Trip’s prepare method.

This tension between the costs of concretion and the costs of abstraction is fundamental to object-oriented design. Concrete code is easy to understand but costly to extend. Abstract code may initially seem more obscure but, once understood, is far easier to change. Use of a duck type moves your code along the scale from more concrete to more abstract, making the code easier to extend but casting a veil over the underlying class of the duck.

Про полиморфизм:

Polymorphism in OOP refers to the ability of many different objects to respond to the same message. Senders of the message need not care about the class of the receiver; receivers supply their own specific version of the behavior.

A single message thus has many (poly) forms (morphs).

There are a number of ways to achieve polymorphism; duck typing, as you have surely guessed, is one. Inheritance and behavior sharing (via Ruby modules) are others, but those are topics for the next chapters.

Polymorphic methods honor an implicit bargain; they agree to be interchangeable from the sender’s point of view. Any object implementing a polymorphic method can be substituted for any other; the sender of the message need not know or care about this substitution.

Как находить утиные типы:

Using duck typing relies on your ability to recognize the places where your application would benefit from across-class interfaces. It is relatively easy to implement a duck type; your design challenge is to notice that you need one and to abstract its interface.

Утиные типы легко обнаружить, когда встречаете:

  • case с условием, которое проверяет класс;
  • kind_of?, is_a?;
  • responds_to?.

Use of kind_of?, is_a?, responds_to?, and case statements that switch on your classes indicate the presence of an unidentified duck. In each case the code is effectively saying “I know who you are and because of that I know what you do.” This knowledge exposes a lack of trust in collaborating objects and acts as a millstone around your object’s neck. It introduces dependencies that make code difficult to change.

Just as in Demeter violations, this style of code is an indication that you are missing an object, one whose public interface you have not yet discovered. The fact that the missing object is a duck type instead of a concrete class matters not at all; it’s the interface that matters, not the class of the object that implements it.

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

Конспект POODR. Creating Flexible Interfaces

Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.

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

Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.

Гибкие интерфейсы

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

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

Публичные интерфейсы:

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

Приватные интерфейсы:

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

The public parts of a class are the stable parts; the private parts are the changeable parts. When you mark methods as public or private you tell users of your class upon which methods they may safely depend. When your classes use the public methods of others, you trust those methods to be stable. When you decide to depend on the private methods of others, you understand that you are relying on something that is inherently unstable and are thus increasing the risk of being affected by a distant and unrelated change.

В поисках интерфейса

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

В голову сразу приходят имена классов: Customer, Trip, Route, Bike. Так происходит, потому что это очевидные вещи из реального мира. А в приложении — существительные, у которых будут данные и поведение (domain objects).

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

Первое, что приходит в голову про выбор туров: пусть Customer вызывает Trip#suitable_trips(on_date, of_difficulty, need_bike). Сразу же нужно спросить себя: а должен ли получатель (Trip) уметь отвечать на это сообщение (suitable_trips)?

Похоже, что нет: ок, что Trip ищет туры по дате и сложности, но почему он должен знать хоть что-то о велосипедах?

Фокус на сообщениях меняет основной вопрос дизайна. Вместо «Так, у меня есть класс. Что он будет делать?» появляется «Мне надо отправить это сообщение. Кто должен на него отвечать?».

You don’t send messages because you have objects, you have objects because you send messages.

Пробуем другой вариант: пусть Customer вызывает Trip#suitable_trips(on_date, of_difficulty), а затем для каждого тура проверяет, есть ли доступные велосипеды, вызывая Bicycle#suitable_bicycle(trip_date, route_type).

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

«Что» вместо «как»

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

Пусть Trip знает, как проверить велик и просит Mechanic это сделать. У Trip есть bicycles. Для каждого велосипеда Trip вызывает clean_bicycle, pump_tires, lube_chaing и check_brakes у Mechanic.

Проблема в том, что Trip знает слишком много о том, что и как Mechanic делает. Если в Mechanic появится новый метод проверки велосипеда (check_repair_kit), измениться должен будет Trip.

Сфокусируемся на том, что нужно Trip, а не на том, как это получить. Пусть Trip просто просит Mechanic проверить велосипеды, вызывая Mechanic#prepare_bicycle.

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

Независимый контекст

Контекст — это то, что объект знает об обкружающих его объектах. Сейчас контекст Trip — это существование объекта Mechanic, который умеет отвечать на prepare_bicycles.

Контекст — штука от которой никуда не деться, которую приходится таскать за собой в приложении и тестах. Чем он больше, тем труднее тестировать и повторно использовать объект: работать с Trip без объекта похожего на Mechanic не получится.

The context that an object expects has a direct effect on how difficult it is to reuse. Objects that have a simple context are easy to use and easy to test; they expect few things from their surroundings. Objects that have a complicated context are hard to use and hard to test; they require complicated setup before they can do anything. The best possible situation is for an object to be completely independent of its context. An object that could collaborate with others without knowing who they are or what they do could be reused in novel and unanticipated ways.

Trip ведь хочет быть готовым к поездке, ему все равно, кто будет этим заниматься. Пусть тогда Trip вызывает Mechanic#prepare_trip(self), а тот сам уже готовит велосипеды.

Теперь все знание о том, что и как механики проверяют в велосипедах, живет в Mechanic. А у Trip уменьшился контекст.

В истории с Mechanic мы прошли три этапа:

  • я знаю, что я хочу и как это сделать с твоей помощью (Mechanic#clean_bicycle);
  • я знаю, что я хочу и что ты делаешь (Mechanic#prepare_bicycles);
  • я знаю, что я хочу и полностью доверяю тебе, делай, что нужно (Mechanic#prepare_trip).

Как сообщения открывают новые объекты

Вернемся к проблеме с поиском туров. Это нормально, что Customer отправляет suitable_trips — это именно то, что он хочет. Проблема с получателем — это точно не Trip. Нам нужен какой-то другой объект.

Пусть это будет TripFinder: Customer вызывает TripFinder#suitable_trips(on_date, of_difficulty, need_bike), TripFinder подбирает туры, вызывая Trip#suitable_trips(on_date, of_difficulty) и Bicylce#suitable_bicycle(trip_date, route_type).

Теперь в TripFinder находится все знание о том, что считать подходящим туром. Он знает все нужные правила. Его можно использовать отдельно от Customer.

Как создавать хорошие интерфейсы

Создавайте явные интерфейсы

Your goal is to write code that works today, that can easily be reused, and that can be adapted for unexpected use in the future. Other people will invoke your methods; it is your obligation to communicate which ones are dependable. Every time you create a class, declare its interfaces. Methods in the public interface should

  • Be explicitly identified as such
  • Be more about what than how
  • Have names that, insofar as you can anticipate, will not change
  • Take a hash as an options parameter

Уважайте чужие публичные интерфейсы

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

Минимизируйте контекст

Construct public interfaces with an eye toward minimizing the context they require from others. Keep the what versus how distinction in mind; create public methods that allow senders to get what they want without knowing how your class implements its behavior.

Фокусируйтесь на сообщениях, а не на классах и предметной области. Используйте их, чтобы открывать новые объекты. Ориентируйтесь на «доверительные» сообщения: что, а не как.

Конспект POODR. Managing Dependencies

Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.

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

Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.

Управление зависимостями

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

Знание получателя создает зависимость между объектами. Когда объект «А» зависит от объекта «Б», изменения в «Б» влекут за собой и изменения в «А».

Распознавание зависимостей

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @wheel     = Wheel.new(rim, tire)
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end

  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end

Зависимости объектов:

  • имя другого класса. Gear ждет, что класс Wheel существует.
  • сообщение, которое объект отправляет кому-то помимо self. Gear ждет, что Wheel отвечает на diameter.
  • аргументы сообщения. Gear знает, что Wheel.new принимает rim и tire.
  • порядок аргументов. Gear знает, что первый аргумент Wheel.newrim, второй — tire.

Каждая из этих зависимостей — шанс того, что Gear изменится из-за изменений в Wheel.

Части зависимостей не избежать: объекты ведь должны как-то взаимодействовать. Но большая часть из примера выше — лишние. Из-за них изменения в коде становятся каскадными: поменяли Wheel, затем Gear и так далее.

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

Связь между объектами

Зависимости привязывают Gear к Wheel. Чем больше Gear знает о Wheel, тем сильнее они связаны. Если нужно использовать Gear где-то еще, вы тащите его вместе с Wheel. Когда тестируете Gear, тестируете и Wheel.

When two (or three or more) objects are so tightly coupled that they behave as a unit, it’s impossible to reuse just one. Changes to one object force changes to all. Left unchecked, unmanaged dependencies cause an entire application to become an entangled mess. A day will come when it’s easier to rewrite everything than to change anything.

Другие зависимости

Другой тип зависимостей — объект, знающий объект, который знает другой объект, у которого есть нужный метод. Изменения в любом месте этой цепочки приведут к изменениям в самом первом объекте. Это — Law of Demeter.

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

Как уменьшить количество зависимостей

То, что Wheel используется явно в Gear, значит, что Gear отказывается работать с чем-либо, кроме Wheel. Если в приложении появятся Disk, Cylinder, вычислить gear_inches для них не получится.

Но для gear_inches имеет значение не тип класса, а сообщение — diameter. Значит, Gear ничего не должен знать о Wheel и его инициализации. Все что нужно gear_inches — это объект, отвечающий на diameter.

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog       = cog
    @wheel     = wheel
  end

  def gear_inches
    ratio * wheel.diameter
  end
  # ...
end

# Gear expects a 'Duck' that knows 'diameter'
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

Извлечение Wheel из Gear разорвало зависимость между ними. До рефакторинга Gear зависел от Wheel, от его инициализации и порядка аргументов. После — зависит только от diameter.

This technique is known as dependency injection. …

Using dependency injection to shape code relies on your ability to recognize that the responsibility for knowing the name of a class and the responsibility for knowing the name of a message to send to that class may belong in different objects. Just because Gear needs to send diameter somewhere does not mean that Gear should know about Wheel.

Изоляцией

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

If prevented from achieving perfection, your goals should switch to improving the overall situation by leaving the code better than you found it.

Think of every dependency as an alien bacterium that’s trying to infect your class. Give your class a vigorous immune system; quarantine each dependency. Dependencies are foreign invaders that represent vulnerabilities, and they should be concise, explicit, and isolated.

Если нет возможности избавиться от Wheel в Gear, изолируйте создание Wheel:

class Gear
  # ...

  def gear_inches
    ratio * wheel.diameter
  end

  def wheel
    @wheel ||= Wheel.new(rim, tire)
  end
end

Gear все еще знает слишком много и привязан к Wheel. Но количество зависимостей у gear_inches уменьшилось, и мы явно обозначили зависимость Gear от Wheel.

These coding styles reduce the number of dependencies in gear_inches while publicly exposing Gear’s dependency on Wheel. They reveal dependencies instead of concealing them, lowering the barriers to reuse and making the code easier to refactor when circumstances allow. This change makes the code more agile; it can more easily adapt to the unknown future.

Представьте, что gear_inches стал сложнее и wheel.diameter спрятан внутри него:

def gear_inches
  #... a few lines of scary math
  foo = some_intermediate_result * wheel.diameter
  #... more lines of scary math
end

Сложный gear_inches зависит от Gear, отвечающего на wheel, и от wheel, отвечающего на diameter. Такая внешняя зависимость делает gear_inches хрупким. Изолируйте внешние зависимости:

def gear_inches
  #... a few lines of scary math
  foo = some_intermediate_result * diameter
  #... more lines of scary math
end

def diameter
  wheel.diameter
end

До изменения gear_inches знал, что у wheel есть diameter и зависел от исходящего сообщения wheel.diameter. После изменения gear_inches — более абстрактный, зависит от сообщения, отправляемого себе (self). Если в Wheel поменяется название diameter или возвращаемое значение, изменения в Gear сведутся к одному методу, diameter.

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

От порядка аргументов

Gear#initialize принимает три обязательных аргумента, chainring, cog, wheel. Аргументы должны быть переданы в правильном порядке и никак иначе.

Пользователи Gear зависят от порядка аргументов в initialize. Если он поменяется, все классы, использующие Gear, тоже должны будут измениться.

В таких случаях используйте хэш вместо фиксированного аргументов:

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(args)
    @chainring = args[:chainring]
    @cog       = args[:cog]
    @wheel     = args[:wheel]
  end
end

Такой вариант убирает зависимость от порядка аргументов и добавляет ясности в вызовы Gear#initialize: new Gear(chainring: 40, cog: 18, wheel: 25).

Если нет контроля над Gear (например, это часть фреймворка), а приложение во многих местах использует его, изолируйте создание новых Gear:

# When Gear is part of an external interface
module SomeFramework
  class Gear
    attr_reader :chainring, :cog, :wheel
    def initialize(chainring, cog, wheel)
      @chainring = chainring
      @cog       = cog
      @wheel     = wheel
    end
  # ...
  end
end

# wrap the interface to protect yourself from changes
module GearWrapper
  def self.gear(args)
    SomeFramework::Gear.new(args[:chainring],
                            args[:cog],
                            args[:wheel])
  end
end

Направление зависимостей

В примерах выше Gear зависит от Wheel или diameter. Эту зависимость можно развернуть в обратную сторону:

class Gear
  attr_reader :chainring, :cog
  def initialize(chainring, cog)
    @chainring = chainring
    @cog       = cog
  end

  def gear_inches(diameter)
    ratio * diameter
  end

  def ratio
    chainring / cog.to_f
  end
  # ...
end

class Wheel
  attr_reader :rim, :tire, :gear
  def initialize(rim, tire, chainring)
    @rim  = rim
    @tire = tire
    @gear = Gear.new(chainring, cog)
  end

  def diameter
    rim + (tire * 2)
  end

  def gear_inches
    gear.gear_inches(diameter)
  end
  # ...
end

Wheel.new(26, 1.5, 52, 11).gear_inches

Выбирайте направление зависимости так, чтобы зависимость менялась реже, чем текущий класс:

Pretend for a moment that your classes are people. If you were to give them advice about how to behave you would tell them to depend on things that change less often than you do.

Это правило базируется на трех аксиомах:

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

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

The second idea concerns itself with the concreteness and abstractness of code. The term abstract is used here just as Merriam-Webster defines it, as “disassociated from any specific instance,” and, as so many things in Ruby, represents an idea about code as opposed to a specific technical restriction.

The wonderful thing about abstractions is that they represent common, stable qualities. They are less likely to change than are the concrete classes from which they were extracted. Depending on an abstraction is always safer than depending on a concretion because by its very nature, the abstraction is more stable

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

Зона B — нейтральные классы, мало зависимостей, вряд ли будут меняться.

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

В хорошем приложении классы будут раскиданы по зонам A, B и C. Зона D — зона смерти. В ней классы, которые будут меняться, и от которых зависит много других классов в системе. Любые изменения в этой зоне слишком дорогие.

The key to managing dependencies is to control their direction. The road to maintenance nirvana is paved with classes that depend on things that change less often than they do.

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

Конспект POODR. Designing Classes with a Single Responsibility

Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.

Это настолько полезная книга, что я перечитываю ее каждый год. Чтобы перестать уже ее перечитывать, хочу закрепить знания с помощью конспекта. В этом посте — конспект второй главы, Designing Classes with a Single Responsibility.

Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.

Дизайн классов

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

At this stage your first obligation is to take a deep breath and insist that it be simple. Your goal is to model your application, using classes, such that it does what it is supposed to do right now and is also easy to change later.

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

You will never know less than you know right now. If your application succeeds many of the decisions you make today will need to be changed later. When that day comes, your ability to successfully make those changes will be determined by your application’s design.

«Легко менять» — это, когда:

  • у изменений нет сайд-эффектов;
  • малые изменения в требованиях → малые изменения в коде;
  • код легко использовать в других местах;
  • самый простой способ внести изменения — добавить код, который так же легко менять.

Код, который легко менять:

  • Прозрачный (Transparent). Последствия изменений очевидны в коде и его зависимостях.
  • Рациональный (Reasonable). Стоимость изменений соразмерна прибыли от этих изменений.
  • Удобный (Usable). Применим в новых, нестандартных контекстах.
  • Образцовый (Exemplary). Воодушевляет тех, кто работает с ним, писать код прозрачным, рациональным и удобным.

Первый шаг к таком коду — принцип единственной ответственности.

Единственная ответственность

Класс должен делать как можно меньше, иметь одну единственную ответственность.

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

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

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

Because the class you’re reusing is confused about what it does and contains several tangled up responsibilities, it has many reasons to change. It may change for a reason that is unrelated to your use of it, and each time it changes there’s a possibility of breaking every class that depends on it. You increase your application’s chance of breaking unexpectedly if you depend on classes that do too much.

Как определить ответственности класса

К нам пришел друг за приложением, считающим всякие штуки для велосипедистов:

class Gear
  # Gear - передача в велосипеде.
  #
  # * chainring — количество зубьев в передней звездочке
  # * rog - количество зубьев в задней звездочке
  # * rim - диаметр обода
  # * tire - диаметр шины
  #
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @rim       = rim
    @tire      = tire
  end

  # Передаточное число. Например, для 52/11 - 4.73.
  # Каждый раз, когда педали делают полный оборот,
  # заднее колесо делает почти 5 оборотов.
  #
  # Чем выше число, тем труднее крутить. Чем ниже, тем легче.
  def ratio
    chainring / cog.to_f
  end
 
  # Велосипедисты в США для сравнения велосипедов используют "gear inches":
  #
  #     диаметр колеса * передаточное число
  #
  def gear_inches
    # tire goes around rim twice for diameter
    ratio * (rim + (tire * 2))
  end
end

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

Попробуйте и поговорить с классом. “Please Mr. Gear, what is your ratio?” — нормально, “Please Mr. Gear, what is your tire (size)?” — звучит дебильно.

Эта концепция — связность (cohesion). Когда все в классе относится к его ответственности — это сильно связанный класс с единственной ответственностью. Это хороший класс.

Как описать ответственность Gear? «Вычислять передаточное число»? Зачем тогда #gear_inches? Класс делает слишком много.

Когда менять дизайн

Мы знаем, что с Gear что-то не так. Может, Gear — это вообще Bicycle? Или где-то спрятался Wheel? Как принять решение?

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

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

Do not feel compelled to make design decisions prematurely. Resist, even if you fear your code would dismay the design gurus. When faced with an imperfect and muddled class like Gear, ask yourself: “What is the future cost of doing nothing today?”

When the future cost of doing nothing is the same as the current cost, postpone the decision. Make the decision only when you must with the information you have at that time.

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

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

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

Спросите себя: «Что будет, если я ничего не стану менять сегодня? Какова цена?».

Этот конфликт между «изменить дизайн сейчас» и «изменить дизайн позже» — вечный. Нет приложений с идеальным дизайном. Хороший программист снижает издержки, принимая дизайнерские решения на основе того, что нужно сейчас, и что может потребоваться в будущем.

Код, устойчивый к изменениям

Опирайтесь на поведение, а не на данные

Поведение зашито в метод. Вы используете его, отправляя сообщения — вызывая метод.

Когда у класса одна ответственность, каждое поведение определено один раз (DRY). Тогда любое изменение поведения — изменение кода в одном месте.

Кроме поведения объекты хранят данные в переменных экземпляра (строки, числа, хэши). Мы обращаемся к данным напрямую через @foo или через вспомогательные методы (attr_accessor).

Используйте вспомогательные методы вместо обращения напрямую:

# плохо
def ratio
  @chainring / @cog.to_f
end
 
# хорошо
attr_reader :chainring, :cog
def ratio
  chainring / cog.to_f
end

Теперь cog — поведение, определенное один раз, а не данные, на которые может быть много ссылок. Если мы использовали @cog в десяти местах, и внезапно условия поменялись, придется сделать 10 изменений. Если cog — метод, потребуется лишь одно изменение:

def cog
  @cog * (foo? ? bar_adjustment : baz_adjustment)
end

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

Regardless of how far your thoughts move in this direction, you should hide data from yourself. Doing so protects the code from being affected by unexpected changes. Data very often has behavior that you don’t yet know about. Send messages to access variables, even if you think of them as data.

Прячьте структуры данных

Рассчитывать на определенную структуру еще хуже, чем работать с данными напрямую:

class ObscuringReferences
  attr_reader :data
  def initialize(data)
    @data = data
  end

  def diameters
    # 0 is rim, 1 is tire
    data.collect { |cell| cell[0] + (cell[1] * 2) }
  end
end

@data — структура данных. Чтобы правильно работать с ней, методы должны знать ее устройство: что и по какому индексу располагается.

diameters знает не только, как считать диаметр, но и где искать данные о диаметре обода и шины.

diameters зависит от структуры @data, если она поменяется — придется поменять и diameters.

Прячьте сложные структуры данных за объектами:

class RevealingReferences
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameters
    wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) }
  end
  # ... now everyone can send rim/tire to wheel

  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect { |cell| Wheel.new(cell[0], cell[1]) }
  end
end

Теперь diameters не знает ничего об устройстве @data.

diameters знает лишь, что wheelsEnumerable, а каждый его элемент отвечает на сообщения rim и tire. Все знание структуры @data теперь изолировано в wheelify. Если структура поменяется, код изменится в одном месте — в wheelify.

Единственная ответственность в методах

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

def gear_inches
  ratio * (rim + (tire * 2))
end

В этом методе несколько ответственностей: внутри gear_inches спрятано вычисление диаметра колеса:

def gear_inches
  ratio * diameter
end
 
def diameter
  rim + (tire * 2)
end

Вытащенный diameter помог определить ответственности класса и вскрыл проблему: ок, Gear вычисляет gear_inches, но diameter он точно не должен вычислять.

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

В Gear спрятался еще один класс, Wheel. Если возможно — вытащите его, если нет — используйте Struct:

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @wheel     = Wheel.new(rim, tire)
  end

  def ratio
    chainring / cog.to_f
  end
 
  def gear_inches
    ratio * wheel.diameter
  end

  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end

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

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

Конспект POODR. Object-Oriented Design

Некоторые ребята уверены, что ООП — это про инкапсуляцию, наследование и полиморфизм, а сами объекты — просто такая обертка над данными. Это не так, ООП — это про сообщения, которые объекты отправляют друг другу. А лучше всего об этом рассказано в POODR, Practical Object-Oriented Design in Ruby Сэнди Метц.

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

Осторожно: это мой субъективный конспект. Не забудьте прочитать оригинал, книга того стоит.

Объектно-ориентированный дизайн

Объектно-ориентированный дизайн (ООД) рассматривает мир как серию сообщений, передаваемых между объектами.

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

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

Изменений не избежать. Задача дизайна — снизить цену изменений.

Changing requirements are the programming equivalent of friction and gravity. They introduce forces that apply sudden and unexpected pressures that work against the best-laid plans. It is the need for change that makes design matter.

Agile processes guarantee change and your ability to make these changes depends on your application’s design. If you cannot write well-designed code you’ll have to rewrite your application during every iteration.

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

Почему тяжело вносить изменения

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

ООД — это про управление зависимостями. Это набор техник программирования, которые помогают организовывать зависимости так, чтобы объекты было легко менять.

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

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

Практическое определение

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

Design is not an assembly line where similarly trained workers construct identical widgets; it’s a studio where like-minded artists sculpt custom applications. Design is thus an art, the art of arranging code.

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

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

Технический долг

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

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

Ищу толкового стажёра

Меня зовут Василий Половнёв, я веб-разработчик. Ищу толкового стажёра, который вместе со мной будет делать крутые бэкендерские штуки и со временем станет тимлидом.

Я ищу стажёра, который:

  • отличает GET от POST, SELECT от UPDATE;
  • отличает git rebase от git merge, пулреквест от реверта;
  • знаком с Рельсами на уровне «написал блог за 15 минут, а потом пилил его по выходным»;
  • слышал про RSpec, модульные и интеграционные тесты;
  • не боится ни легаси, ни ПХП, ни фронтенда.

За полтора месяца научу стажёра:

  • проверять свою работу модульными и интеграционными тестами;
  • писать тесты быстрее, чем код;
  • ревьюить, парно программировать и не унывать;
  • быстро работать с Гитхабом, Рельсами и Семафором;
  • находить и исправлять «запахи» в коде: от Long Method до Shotgun Surgery.

Для этого стажёр будет:

  • работать 20 часов в неделю, часть времени — в паре со мной;
  • тратить 2-3 часа в неделю на теорию и домашние задания;
  • переписывать код снова и снова после ревью.

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

Если написанное выше про вас, напишите мне письмо с рассказом о себе на vasily@polovnyov.ru.

Если нет, поделитесь постом в соцсетях. Вот картинка для Фейсбука:

Что почитать начинающему тимлиду

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

Базовый минимум

Процессы и продуктоводство

Крутаны

Посоветуйте, что ещё почитать, чтобы стать тимлидом, который нужен Готэму:
vasily@polovnyov.ru

Советы

С марта выходят мои советы на сайте Дизайн-бюро Артёма Горбунова. В них я рассказываю, как устроены технические штуки в бюро, и даю советы о тестировании, производительности и веб-разработке.

Пока вышло четыре совета:

А затем еще несколько клевых:

И пачка советов дизайнерам:

Присылайте вопросы. Буду рад помочь.

Сравнение CI-сервисов

На прошлой неделе меня перестал устраивать Semaphore за $29 в месяц, и я отправился на поиски CI-сервиса подешевле.

На всех опробованных сервисах я использовал один и тот же проект: Rails, RSpec, Rubocop. Сравнивал цены за 10-12 проектов и время, потраченное на билд.

Длительность билда Цена в месяц
Semaphore 1:08 $29
Vexor 1:48 ¯\_(ツ)_/¯
$0.015 за минуту
Travis CI 2:25 $69
Snap 2:40 $80
CircleCI 3:23 $50 за 2 контейнера
$0 за контейнер и 1500 минут
Codeship 3:48 $49
MagnumCI $0
Solano CI $15
Shippable $25
Drone $49

В последних четырех сервисах я не смог запустить тестовый билд. Solano CI и Shippable не увидели приватных проектов. В MagnumCI и Drone не было Руби 2.3, а есть ли ruby-build или rvm — непонятно.

Самый быстрый — Semaphore. Из-за локального зеркала RubyGems и кучи предустановленных версий Руби. Тот же Travis 20 секунд потратил на установку Руби и 94 секунды — на установку гемов.

Самый дешевый — бесплатный CircleCI, если влезете в ограничение на 1500 минут в месяц. Если не влезете, посмотрите Vexor. Меня они смутили тем, что билды тупо не запускались, пока ребята не «перезагрузили сервер».

Что я выбрал

Semaphore. Чем быстрее работает CI, тем быстрее выкатываются фичи. Кроме того, я попросил у них скидку, и они дали 20%.

Как тестировать коллбэки ActiveRecord моделей

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

class Post < ApplicationRecord
  before_save :set_permalink
  after_save :rebuild_preview

  private

  def set_permalink
    self.permalink ||= build_permalink
  end

  def build_permalink
    date.strftime("%Y/%-m/") + title.parameterize("-")
  end

  def rebuild_preview
    Preview.new(self).rebuild
  end
end

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

Тестируйте публичный интерфейс, использующий эти приватные методы. Если коллбэк на after_save/before_save, тестируйте save, если на after_initialize/before_initializenew.

В случае с постом спека выглядела бы так:

describe "#save"
  context "when permalink is NOT set" do
    it "generates it from year, month and title" do
      post = Post.create(
        date: new Date(2016, 07, 07),
        title: "Hello, its me")

      expect(post.permalink).to eq "2016/7/hello-its-me"
    end
  end

  context "when permalink is set" do
    it "preserves it" do
      post = Post.new(
        title: "Some title",
        permalink: "some-special-post")

      expect { post.save }.not_to change { post.permalink }
    end
  end

  it "rebuilds preview" do
    preview = instance_double(Preview, rebuild: true)
    post = Post.new(title: "Some title")
    allow(Preview).to receive(:new).and_return(preview)

    post.save

    expect(preview).to have_received(:rebuild)
  end
end

Вот и все дела.

Гигиена в тестах

Бывает, вижу вот такое:

describe "callbacks" do
  describe "before validation" do
    let(:blog) {create(:blog) }
    context "when slug is not set" do
      it "generates it" do
        item = Post.new(blog: blog)
        item.validate
        expect(item.slug).not_to be_nil
      end
    end
    context "when slug is set" do
      it "does not change it" do
        item = Post.new({blog: blog })
        item.slug = 'in the darkness'
        item.validate
        expect(item.slug).to eq 'in the darkness'
      end
    end
  end
end

Читать такую простыню не хочется. А странности в форматировании заставляют глаз спотыкаться.

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

Разделяйте фазы

Тест состоит из трех фаз: подготовка, испытание и проверка. Отбивайте их друг от друга пустой строкой. Так же отбивайте context/describe/it блоки:

describe "callbacks" do
  describe "before validation" do
    let(:blog) {create(:blog) }

    context "when slug is not set" do
      it "generates it" do
        item = Post.new(blog: blog)

        item.validate

        expect(item.slug).not_to be_nil
      end
    end

    context "when slug is set" do
      it "does not change it" do
        item = Post.new({blog: blog })
        item.slug = 'in the darkness'

        item.validate

        expect(item.slug).to eq 'in the darkness'
      end
    end
  end
end

Пишите в одном стиле

Чтобы писать единообразно и не вспоминать, какие кавычки надо использовать, накрутите Rubocop. Потом поставьте Sublime-Linter и SublimeLinter-rubocop, чтобы видеть ошибки прям в редакторе:

Тесты станут читаться без запинки:

describe "callbacks" do
  describe "before validation" do
    let(:blog) { create(:blog) }

    context "when slug is not set" do
      it "generates it" do
        item = Post.new(blog: blog)

        item.validate

        expect(item.slug).not_to be_nil
      end
    end

    context "when slug is set" do
      it "does not change it" do
        item = Post.new(blog: blog)
        item.slug = "in the darkness"

        item.validate

        expect(item.slug).to eq "in the darkness"
      end
    end
  end
end

Анти-паттерны в тестах

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

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

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

Неполные тесты

Непонятные тесты

Ненужные тесты

Нереалистичные тесты

Ненадежные тесты

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

Теперь вы знаете, что почитать на выходных.

Проблемы с subject

subjectхелпер в RSpec для однострочных тестов. Бывает неявным и явным.

# Неявный subject
RSpec.describe User do
  # subject — User.new (described_class.new)
  it { is_expected.to validate_presence_of(:name) }

  # Такой тест разворачивается в:
  it "..." do
    expect(User.new).to validate_presence_of(:name)
  end
end

# Явный subject
RSpec.describe User do
  subject { described_class.new(role: "admin") }

  it { is_expected.to be_super_admin }
end

В однострочниках

subject здорово сокращает тесты валидаций и ассоциаций в Рельсах с Shoulda Matchers:

RSpec.describe ActivityLog do
  it { is_expected.to validate_presence_of(:user_name) }
  it { is_expected.to validate_presence_of(:event) }
  it { is_expected.to validate_presence_of(:ip) }

  it { is_expected.to belong_to(:user) }
end

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

RSpec.describe Alarm do
  describe "#snooze" do
    subject { described_class.new(at: time) }

    it { expect { subject.snooze }.to change { subject.at }.by(9.minutes)
  end
end

Не помогает и именованный subject:

RSpec.describe Alarm do
  describe "#snooze" do
    subject(:alarm) { described_class.new(at: time) }

    it { expect { alarm.snooze }.to change { alarm.at }.by(9.minutes)
  end
end

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

it { expect(...) } — плохо

Чтобы исправить ситуацию, разверните тест и добавьте описание:

# Ясно, что #snooze передвинет будильник на 9 минут вперед,
# без чтения проверки
it "pauses alarm for next 9 minutes" do
  expect {
    subject.snooze
  }.to change {
    subject.at
  }.by(9.minutes)
end
subject для проверок валидаций и ассоциаций в Рельсах — хорошо

В начале спеки

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

RSpec.describe Post do
  let(:author) { build(:author, name: "Melanie C") }
  subject { build(:post, author: author, title: "Awesome News") }

  describe "#slug" do
    it "generates slug from title" do
      expect(subject.slug).to eq "awesome-news"
      # Чтобы понять, почему slug именно такой,
      # придется вернуться в начало спеки и увидеть
      # title у поста
    end
  end

  describe "#author_name" do
    subject { post.author_name }

    context "with author" do
      it { is_expected.to eq "Melanie C" }
      # Чтобы понять, откуда взялась Мелани Си,
      # придется вернуться в начало спеки (и в 90-е, конечно)
    end

    context "without author" do
      let(:author) { nil }

      it { is_expected.to eq "Annoymouse" }
      # Чтобы понять, почему здесь Annoymouse, что
      # и где меняет author = nil,
      # придется вернуться в начало спеки
      # и пройтись по всем describe/let/context
    end
  end
end

Проблема в тестах выше в том, что информация важная для восприятия проверки далеко от нее. Читатель превращается в Шерлока Холмса и тратит время впустую, расследуя, что скрывает subject.

Глобальный subject — плохо

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

# Хорошо: информация нужная для восприятия проверки на месте
describe "#slug" do
  it "generates slug from title" do
    # Нам нет дела до автора, а вот title важен
    post = build(:post, title: "Awesome News")

    expect(post.slug).to eq "awesome-news"
  end
end
# Так себе: информация стала ближе к проверке,
# но чтобы понять смысл проверки,
# придется ее «скомпилировать» в голове
describe "#author_name" do
  context "with author" do
    let(:author) { build(:author, name: "Melanie C") }
    let(:post) { build(:post, author: author) }

    subject { post.author_name }

    it { is_expected.to eq "Melanie C" }
  end
end
# Лучше: информация стала ближе к проверке.
# Описание теста ясно дает понять, что проверяем
describe "#author_name" do
  context "with author" do
    let(:author) { build(:author, name: "Melanie C") }
    let(:post) { build(:post, author: author) }

    it "returns author's name" do
      expect(post.author_name).to eq "Melanie C"
    end
  end
end
# Хорошо: информация нужная для восприятия проверки на месте
describe "#author_name" do
  context "with author" do
    it "returns author's name" do
      author = build(:author, name: "Melanie C")
      post = build(:post, author: author)

      expect(post.author_name).to eq "Melanie C"
    end
  end
end
Держать информацию нужную для понимания проверки рядом с ней — хорошо

Дополнительное чтение

По хардкору: describe и context

describe и context объединяют связанные проверки и задают структуру теста.

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

Без структуры:

it "#accessible_projects return all system projects when user is admin"
it "returns no projects when user is not admin"
it "returns user full name with email"

Со структурой:

describe "#accessible_projects" do
  context "when user is admin" do
    it "returns all system projects"
  context "when user is NOT admin" do
    it "returns no projects"

describe "#user_name_with_email" do
  it "returns user full name with email in parens"

Когда и что использовать

Я использую describe для сущностей (кто, что?), а context для состояний (когда, с чем, с какими условиями?). Так легче ориентироваться в возможных ситуациях и исследовать условия.

# Так себе
context "associations"
context "#full_name"
describe "when user is admin"
describe "with valid params"

# Хорошо
describe "associations"
describe "#full_name"
context "when user is admin"
context "with invalid params"

Контексты начинаю со слов when, if, with, given. Если вижу их в описании проверки — вытаскиваю:

# Плохо: условие, описание ситуации
# (user is editor) тяжело заметить
it "returns only published posts when user is not editor"
it "returns published and draft posts when user is editor"

# Хорошо: ситуация явно обозначена контекстом
context "when user is editor" do
  it "returns published and draft posts"

context "when user is NOT editor" do
  it "returns only published posts"

Вложенные контексты начинаю с and, чтобы было понятно, что это не все условие:

context "when user is editor" do
  context "and post is published" do
    it "displays post views/visitors count"

Шпаргалка

describe <something>
context <when... / if... / with... / given...>

Внимательный читатель со звездочкой залезет в исходники RSpec, чтобы убедиться, что describe и context отличаются только по смыслу.

Стоит почитать

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

ЖЗЛ

Арнольд Шварценеггер. Вспомнить все: моя невероятно правдивая история

Зачем читать: чтобы посмотреть, насколько полной и насыщенной жизнью можно жить.

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

Теодор Драйзер. Финансист

Зачем читать: чтобы посмотреть на жизнь несгибаемого прагматика.

Джек Лондон. Время-не-ждет

О силе характера и бесполезности самоубийства.

Ральф Лейтон и Ричард Филлипс Фейнман. Вы, конечно, шутите мистер Фейнман

О задротстве и чувстве юмора.

Ричард Филлипс Фейнман. Какое ТЕБЕ дело до того, что думают другие?

Зачем читать: ради удивительной истории любви.

Работа

Джим Кэмп. Сначала скажите «нет»

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

Гэвин Кеннеди. Договориться можно обо всем

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

Карл Сьюэлл. Клиенты на всю жизнь

Зачем читать: чтобы научиться клиентскому сервису.

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

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

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

Лучшая в мире система клиентского сервиса является и самой простой: ДЕЛАЙ ТО, ЧТО ОБЕЩАЛ, И ДЕЛАЙ ЭТО С ПЕРВОГО РАЗА.

Майкл Гербер. Создание предприятия которое бы работало (П-Миф)

Зачем читать: чтобы познакомиться с Менеджером, Специалистом и Предпринимателем, живущим в вас.

Максим Батырев. 45 татуировок менеджера

Зачем читать: чтобы быстро прокачать внутреннего Менеджера.

Кармин Галло. iПрезентация

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

Сделывание

Брайан Моран и Майкл Леннингтон. 12 недель в году. Как за 12 недель сделать больше, чем другие успевают за 12 месяцев

Зачем читать: чтобы научиться планировать на квартал и неделю вперед. Стратегия.

Марк Форстер. Сделайте это завтра

Лучшая книга по планированию и «сделыванию» ежедневных задач. Тактика.

Дэвид Шварц. Искусство мыслить масштабно

Почаще напоминайте себе: «Лучше износиться, чем проржаветь».

Действие убивает страх! С другой стороны, колебание, нерешительность, отсрочка только усиливают его.

Очень неуютно быть среди полуживых, постоянно жалующихся на все зануд.

Программирование

Сенди Мэтц. Practical Object-Oriented Design in Ruby

Зачем читать: чтобы найти ключ к пониманию ООП, дизайну ПО и тестированию.

These benefits, however valid, are proxies for a deeper goal. The true purpose of testing, just like the true purpose of design, is to reduce costs. If writing, maintaining, and running tests consumes more time than would otherwise be needed to fix bugs, write documentation, and design applications tests are clearly not worth writing and no rational person would argue otherwise

Ищу стажеров (уже нашел)

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

Ищу четверых стажеров — Руби-разработчиков, у которых проблемы с тестами:

  • не понимают, что и как тестировать;
  • прочитали RSpec Book, не могут написать тест с нуля;
  • исправляют баг за 5 минут, потом 2 часа пишут для него тест;
  • пишут тесты, но не могут смотреть на них без боли.

За полтора месяца научу стажеров:

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

Для этого стажеры будут:

  • тратить 3 часа в неделю на теорию и домашние задания;
  • писать тесты на RSpec, Jasmine и Capybara;
  • переписывать тесты снова и снова после ревью.

Если написанное выше про вас, напишите мне письмо с рассказом о себе на vasily@polovnyov.ru.

Если нет, поделитесь постом в соцсетях. Вот смешная картинка:

По хардкору: что писать в комментарии к коммиту

У меня был коллега, который коммиты сопровождал одним и тем же комментарием, изредка играя с заглавными буквами. Его история изменений выглядела так:

Такие комментарии — ад. Они не говорят о том, что изменилось и почему. Они не помогают при ревью и откате изменений. В них нет ни смысла, ни логики, ни пользы.

Чтобы комментарии к коммитам были полезны и вам, и коллегам, договоритесь об их стиле и содержимом. А я пока поделюсь своим вариантом.


На каком языке писать

В коде вы работаете с Post, Blog, User. Вы мыслите ими и не задумываясь используете в речи: добавим юзеру аватарку, покажем популярные посты в блоге.

Комментарии на русском сбивают с толку и заставляют вспоминать, что это и где:

f0b4ac поправил верстку подвала статьи

«Статья». Это у нас что? Article, Post, BlogEntry? Если коммит на английском, проблем нет:

f0b4ac fix post footer markup
Пишите на английском

Что писать в комментарии

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

Полезный комментарий описывает не что было сделано, а результат в мире клиента разработчика:

# Плохо: это мы и в диффе видим.
# Зачем добавляли-то? Что пытались поправить?
d77d9f add <div> wrap

# Хорошо: ясно, зачем сделаны изменения, какую проблему решали
d77d9f fix Firefox issue with flexbox padding
Не что сделано, а зачем

В каком времени

Я рассматриваю историю Гита как историю команд, приказов, приводящих репозиторий из одного состояния в другое. Поэтому пишу в повелительном наклонении. Чтобы было проще писать, использую шаблон-скороговорку:

# If applied, this commit will
mention Ubuntu installation instructions in README
В повелительном наклонении

ПРОТИП: если в комментарии есть and, скорее всего, в коммите несколько несвязанных изменений:

fix ... and update ...
introduce ... and correct ...

Дополнительное чтение

Дезодоранты и полезные комментарии

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

Смотрите:

def archive
  # unlock and destroy everything in project
  discussions.each(&:unlock!)
  todos.each(&:unlock!)
  discussions.destroy_all
  todos.destroy_all

  # assign read-only role to users
  users.each do |user|
    user.update_attribute(role: "client")
  end

  self.status = 38 # set status to archived
end

Чтобы сделать код понятнее, вытащите константу и несколько методов с вразумительными именами:

def archive
  clean_up_before_archiving
  reset_user_roles_to_read_only

  self.status = ARCHIVED
end


def clean_up_before_archiving
  discussions.each(&:unlock!)
  todos.each(&:unlock!)

  discussions.destroy_all
  todos.destroy_all
end

def reset_user_roles_to_read_only
  users.each(&:reset_role)
end

БА ДУМ ТСС! Отсюда правило:

Если код не понять без комментария, рефактори

Полезные комментарии

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

# Monkey patching Paperclip to disable built-in spoofing protection
# which gives many false-positive errors and prevents uploading of
# .xlsx, .caf, .msg files.
#
# For more details see:
#
# * http://robots.thoughtbot.com/prevent-spoofing-with-paperclip
# * https://github.com/thoughtbot/paperclip/issues/1456
# * https://github.com/thoughtbot/paperclip/issues/1530

module Paperclip
  class MediaTypeSpoofDetector
    def spoofed?
      false
    end
  end
end

Или объясняем, для чего здесь этот хак:

.is-visible & {
  // Adding transparent outline fixes jagged edges in Firefox.
  //
  // See: http://stackoverflow.com/a/9333891
  outline: 1px solid rgba(255, 255, 255, 0);
}

Наверняка бывают и другие типы полезных комментариев. Если вы их встречали, покажите-расскажите: vasily@polovnyov.ru.

###

Чтобы этот пост активнее шарили в Фейсбуке, прикладываю картинку-правило, заряженную на рефакторинг и быстрые тесты:

По хардкору: instance_double, verify_partial_doubles

В комментариях к посту о дублерах, стабах и моках Макс Прокопьев упомянул instance_double, секретную технику старших разработчиков. О ней и поговорим. Коротко, по делу и с примерами.

Ситуация

Возьмем пример с Notifications:

class Notifications
  def as_json
    # Загружаем и отдаем последние новости
    # и уведомления с серверов Юрия Лозы по ХТТП
  end
end

class NotificationsController
  def index
    # Оборачиваем уведомления в ключ 'data'
    render json: { data: notifications.as_json }
  end

  def notifications
    Notifications.new
  end
end

Чтобы не обращаться к серверам Лозы в тестах, используем дублера:

describe "NotificationsController#index" do
  let(:notifications) do
    double(:datasource, as_json: { notifications: [] })
  end

  before do
    allow(Notifications)
      .to receive(:new)
      .and_return(notifications)
  end

  it "wraps notifications in 'data' key" do
    get :index, format: :json

    expect(json_response["data"].keys)
      .to have_key "notifications"
  end
end

Что произойдет, если мы переименуем Notifications#as_json в Notifications#to_json? Ничего. Мы останемся наедине с зеленым тестом, проверяющим бесполезного дублера.

instance_double

Чтобы не попадать в такую дебильную ситуацию, используйте instance_double:

describe 'NotificationsController#index' do
  let(:notifications) do
    instance_double(Datasource, as_json: { notifications: [] })
  end

  # ...
end

Такой дублер проверит свой интерфейс. Если метода нет или у него другие аргументы — красный тест.

Чтобы так же проверять моки и стабы, убедитесь, что verify_partial_doubles включен в spec_helper.rb.


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

По хардкору: дублеры, моки, стабы

Сегодня о тестах. Пост для тех, кто знаком с RSpec, но не понимает, что такое «мокать» и «застабить». Коротко, по делу и с примерами.

Дублер (test double)

Объект-каскадер, подменяющий реальный объект системы во время тестов:

describe NotificationsController do
  # NotificationsController загружает последние уведомления
  # со стороннего сервиса по HTTP
  # с помощью NotificationsDatasource.
  let(:datasource) do
    double(:datasource, as_json: { notifications: [] })
  end

  before do
    # Подменяем реальный NotificationsDatasource дублером,
    # чтобы не зависеть от внешнего сервиса в тестах:
    allow(NotificationsDatasource)
      .to receive(:new)
      .and_return(datasource)
  end

  describe "#index" do
    it "wraps notifications in 'data' key" do
      get :index, format: :json

      expect(json_response["data"].keys)
        .to have_key "notifications"
    end
  end
end

Стаб (stub)

Заглушка для метода или объекта, возвращающая заданное значение:

context "when attachment file is too large to email" do
  let(:max_file_size) { Attachment::MAX_FILE_SIZE }

  before do
    allow(attachment)
      .to receive(:file_size)
      .and_return(max_file_size + 1)
  end

  it "raises 'file is too large' error" do
    # ...
  end
end

Внимательный читатель со звездочкой уже заметил, что и в предыдущем примере с NotificationsController был стаб. Все верно: стаб — это дублер с зашитыми ответами.

Мок (mock)

Стаб с ожиданиями, которые RSpec проверит в конце теста:

context "when cloning process cannot be performed" do
  before do
    allow(doctor).to receive(:clone) { raise "can't clone" } # стаб
  end

  it "notifies airbrake" do
    expect(Airbrake).to receive(:notify) # мок
    # Rspec застабит `Airbrake.notify`
    # и в конце этого `it do...end` блока
    # проверит, был ли он вызван.
    # Если вызова не было — ошибка и красный тест.
    #
    # Когда на собеседовании спросят, чем
    # отличается мок от стаба, отвечайте:
    # «Только мок может завалить тест».

    clone_poor_dolly
  end
end

Моки меняют порядок фаз в тесте. Вместо «Подготовка — Испытание — Проверка» получается «Проверка+Подготовка — Испытание». Если вам, как и мне, тяжело такое читать, используйте стаб с проверкой:

# мок
it "notifies airbrake" do
  expect(Airbrake).to receive(:notify) # проверка + настройка

  clone_poor_dolly # испытание
end

# стаб + проверка
it "notifies airbrake" do
  allow(Airbrake).to receive(:notify) # настройка

  clone_poor_dolly # испытание

  expect(Airbrake).to have_received(:notify) # проверка
end

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