Архивы Категорий: Веб-разработка

Главная / Веб-разработка
41 Пост

В этом посте мы затронем создание динамичного контента. Конкретно мы рассмотрим:

  • Как создать контроллер
  • Действия
  • Встроенный Ruby (ERB)

Создание контроллера

Контроллер содержит действия (методы Ruby) и координирует веб-запросы.

Rails может быстро сгенерировать контроллер с 0 или более действий с их ассоциированными представлениями.

Генерация происходит следующим образом:

rails generate controller controller_name [action1 action2]

Команда generate может быть сокращена до g.

Попробуем сгенерировать контроллер greeater, «hello» будет именем действия и именем представления.

Таким образом, мы получаем контроллер greeter.rb в папке app/controllers. Также получаем файл app/views/greeter/hello.html.erb.

Если вы заглянете в этот файл представления, вы увидите, что там нет типичных для HTML элементов Doctype, head или body. Мы увидим немного позже, почему.

Как это выглядит?

Встроенный Ruby (ERB)

Сгенерированный файл представления выглядит как типичный HTML-файл, но он также имеет расширение .erb.

ERB это библиотека шаблонизации (похожая на JSP, к примеру), которая позволяет вам встраивать Ruby в ваш HTML.

Стоит выучить два паттерна:

  • <% ...код ruby... %> — рассчитывает код
  • <%= ...код ruby... %> — возвращает рассчитанный код

И еще раз, идея ERB в том, чтобы иметь возможность включать динамичный контент в статичный HTML-контент, чтобы смешивать их вместе.

Так что, к примеру, мы можем переписать нашу приветственную страницу.

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

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

Как выглядит контроллер

Итак, это была часть про представление. Как же выглядит сам контроллер?

hello это просто действие. Это обычный метод Ruby, в данном случае пустой.

И это работает.

А что если мы хотим добавить действие «goodbay» в этот контроллер, и также goodbay.html.erb в папку app/views/greeter?

Давайте попробуем.

Создаем файл для view в соответствующей папке, затем редактируем контроллер.

Заходим на страницу /greeter/goodbay:

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

В итоге

Контроллеры содержат действия (методы).

ERB позволяет рассчитывать выражения с помощью паттерна <% expression %> или выводить их с помощью <%= expression %>.

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

Чтобы создать свое первое приложение, перейдите в каталог, в котором хотите его создать. Введите rails new my_first_app, где «my_first_app» — имя приложения.

Это создаст определенную структуру, о которой я расскажу немного ниже.

Вы также можете выполнить rails new -h, в этом случае вы увидите какие параметры вы могли бы указать при создании нового Rails-приложения.

Bundler (менеджер gem’ов)

Ближе к концу создания приложения вы увидите «bundle install». Его идея заключается в том, что в приложении у вас имеется множество разных компонентов, которые объединяются вместе и имеют разные версии, и, очевидно, они должны взаимодействовать.

Один компонент может зависеть от одной версии, другой компонент от другой. И вам нужно разрешить эти зависимости, — это то что делает Bundler.

Контроль версий вашего приложения

Итак, сейчас ваше приложение создано. Rails сгенерировал изначальный скелет и даже немного кода.

Также интересно, что Rails автоматически создает файл .gitignore, что показывает, что он подталкивает вас к использованию Git.

Создаем репозиторий и делаем наш первый коммит:

cd my_first_app
git init
git add .
git commit -m "Initial commit"

Запускаем приложение

Кроме создания скелета приложения, Rails также имеет встроенный веб-сервер.

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

Опционально открываем новое окно терминала, переходим в нужную директорию (cd my_first_app) и выполняем:

rails server

Либо rails s, что является той же командой в сокращенном виде.

Здесь говорится о версии Rails, о режиме в котором вы находитесь (development, т.е. разработки) и адресе по которому запущено приложение (http://localhost:3000).

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

Когда вы развернете ваше приложение, к примеру на Heroku, оно будет работать в режиме production.

Открываем в браузере localhost:3000, и Rails уже приветствует нас.

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

Конвенция структуры директорий

Итак, приложение будет иметь директорию app, на которую вы будете тратить большую часть времени. Она включает модели, контроллеры и представления (папки controllers, models и views), а также хелперы и прочее.

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

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

Папка public содержит статичные файлы. К примеру, HTML-файлы, которые вы редко будете изменять, пойдут в эту папку.

Gemfile и Gemfile.lock используются Bundler’ом для разрешения зависимостей. Здесь вы указываете нужные gem’ы.

public / hello_static.html

Мы упоминали, что статичные файлы идут в папку public.

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

Так что, если вы хотите добавить полностью статичную страницу, вы добавляете её в эту папку.

Попробуем добавить такой файл.

Переходим на localhost:3000/hello_static.html. Если ваш сервер уже запущен, нет нужды перезапускать его.

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

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

В итоге

rails new создает новое приложение.

rails server / rails s запускает его (нужно выполнять в папке с самим приложением).

Статичные страницы находятся в папке public.

Мы рассмотрели Ruby, теперь можно переходить к Ruby on Rails. В этом и следующих постах мы будем обсуждать основные принципы, конвенции и MVC-структуру.

Итак, в этом посте я расскажу о следующих вещах:

  • Краткая история Rails
  • Польза от использования Rails
  • Model View Controller

История Ruby on Rails

Rails (RoR) — это фреймворк для разработки динамичных веб-приложений.

Он был создан в 2004-2005 годах Давидом Ханссоном (который, кроме того, является автогонщиком).

Кто использует Rails?

Так кто использует Rails? Много больших, а также средних сайтов, таких как Hulu, Groupon, LivingSocial, Twitter и Github.

Стартапы в целом любят Rails из-за его возможности быстро прототипировать и быстро разрабатывать приложения.

Почему Rails?

Один из принципов использования Rails называется Convention Over Configuration.

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

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

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

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

Так что, вы будете знать чего ожидать в следующий раз.

Следующее преимущество Rails — это то что у вас есть уровень абстракции базы данных. Ruby on Rails позволяет вам взаимодействовать с базой данных не путем написания SQL-запросов, вы можете взаимодействовать с базой данных путем написания Ruby-кода.

Это иногда называется ORM, то есть объектно-реляционное сопоставление.

Вы сопоставляете свою базу данных с вашими классами Ruby.

Кроме того, Rails:

  • Благоприятен для гибкой разработки
  • Следует принципу «не повторяйся» (Don’t repeat yourself)
  • Кроссплатформенный
  • С открытым исходным кодом
  • Модульный

SQLite

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

SQLite — это автономная, безсерверная, транзакционная база данных SQL нулевой конфигурации.

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

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

Когда вы создаете Rails-приложение, возможность взаимодействия с базой данных находится прямо «в коробке».

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

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

MVC: Module, View, Controller

Еще одна концепция, которую использует Rails, — это идея Model View Controller. На русский это переводится как «Модель-Вид-Контроллер» или «Модель-Представление-Контроллер».

MVC изобрел норвержский профессор Трюгве Реенскауг в 1979 году.

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

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

View — визуальное представление этих данных.

Controller — координирует взаимодействие между моделью и представлением.

Цикл MVC

  1. Отправлен запрос
  2. Контроллер ← → Модель
  3. Контроллер запускает Представление
  4. Представление отображает данные

MVC в Rails

В итоге

В итоге, Rails очень хорошо подходит для быстрого прототипирования.

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

В этом посте мы продолжим разбирать RSpec и поговорим о matchers.

«Matchers» не переводится с английского, но слово match в этом контексте означает «совпадать, равняться».

Итак.

RSpec «вешает» методы to и not_to на все результаты ожиданий.

Эти два метода принимают один параметр — matcher.

Примеры matcher’ов:

  • be_true / be_false
  • eq 3
  • raise_error(SomeError)

Be_predicate — логическое значение

Если объект, на котором проводится тест, имеет утвердительный (логический) метод, вы автоматически получите матчер be_predicate.

Так что, к примеру, be_nil является валидным матчером, так как каждый объект в Ruby имеет метод :nil?.

В нашем примере с калькулятором мы можем иметь такой тест:


it "should sum two odd numbers and become even" do
 expect(@calculator.add(3, 3)).to be_even
 expect(@calculator.add(3, 3)).not_to be_odd
end

Это полностью валидно и выглядит почти как естественный английский.

Так как мы исправили ошибки в калькуляторе, все тесты работают.

Но если вы хотите узнать подробнее, что работает, а что нет, введите rspec --format documentation или rspec -f d:

Больше матчеров

Полный список матчеров можно найти в документации RSpec.

https://relishapp.com/rspec/rspec-expectations/v/3-6/docs/built-in-matchers

В итоге

RSpec имеет множество встроенных матчеров, которые доступны для упрощения написания тестов, я думаю это отличный фреймворк для тестирования приложений.

Хотя если вы предпочитаете MiniTest, используйте его.

В этом посте мы продолжим говорить о юнит-тестировании в Ruby и рассмотрим RSpec.

Тестирование с помощью RSpec

Test::Unit делает свою работу, но было бы неплохо, если бы тесты были более описательными, более похожими на естественный английский.

RSpec позволяет это.

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

Установка RSpec

Устанавливаем RSpec командой gem install rspec.

describe()

Метод describe() это набор связанных тестов.

Он принимает строку либо класс в качестве аргумента.

Все тесты должны быть внутри этого блока.

Здесь нет подкласса для класcа (в отличие от Test::Unit), всё происходит внутри метода describe().

Методы before() и after()

Методы before() и after() похожи на setup() и teardown() в MiniTest.

Они принимают значения :each или :all, чтобы указать будет блок выполняться перед/после каждого теста или один раз перед/после всех тестов.

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

Метод it()

Используется для определения фактических спецификаций/примеров RSpec.

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

Пример

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


class Calculator

  attr_reader :name

  def initialize(name)
  	@name = name
  end

  def add(one, two)
  	one - two # скажем, вы допустили ошибку здесь (- вместо +)
  end

  def subtract(one, two)
  	one + two # и в этом методе
  end

  def divide(one, two)
  	one / two
  end
end

Выполним в терминале команду rspec --init в нужной папке, чтобы всё было подготовлено.

spec/calculator_spec.rb:


require 'rspec'
require_relative '../calculator' # или calculator.rb, без разницы

describe Calculator do # здесь вместо класса Calculator можно быть бы написать строку, но здесь это не имеет смысла
  before { @calculator = Calculator.new('RSpec calculator')}

# Фактические спецификации:

  it "should add 2 numbers correctly" do
  	expect(@calculator.add(2, 2)).to eq 4 
  end
  	
  it "should subtract 2 numbers correctly" do
   expect(@calculator.subtract(4, 2)).to eq 2 
  end  

  it "should sum two odd numbers and become even" do
   expect(@calculator.add(3, 3)).to be_even 
   expect(@calculator.add(3, 3)).not_to be_odd 
  end

end

Как мы видим, RSpec более интуитивный и больше похож на естественный английский.

Выполняем команду rspec.

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

В итоге, если вам нравится MiniTest, можете использовать его. Если вам нравится выразительность RSpec, попробуйте это.

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

В этом посте мы рассмотрим юнит-тестирование (модульное тестирование).

Идея юнит-тестирования заключается в написании тестового кода, который проверяет ваш живой код.

Так почему люди пишут юнит-тесты?

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

Это особенно верно для Ruby, который является динамическим языком. В языке статического типа, таком как Java, если вы делаете опечатку, ваш редактор, вероятно, скажет вам об этом.

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

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

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

Test::Unit

Ruby очень серьезно относится к тестированию и предоставляет для этого фреймворк Test::Unit.

В Ruby 1.8 Unit::Test был загружен лишними библиотеками, которые включали много ненужного кода.

Ruby 1.9 уменьшил Test::Unit до необходимого минимума.

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

Обзор Test::Unit

  • Test::Unit входит в семью XUnit (наряду с JUnit, CppUnit)
  • Основная идея — расширение Test::Unit::TestCase
  • Перед именем метода приставляется test_
  • Если один из методов не удается, другие продолжают проверяться (это хорошая вещь)
  • Можно использовать методы setup() и teardown() для установки поведения, которое будет выполняться перед каждым тест-методом

Давайте посмотрим на реальный пример. Скажем, вы пишите калькулятор.


class Calculator

  attr_reader :name

  def initialize(name)
  	@name = name
  end

  def add(one, two)
  	one - two # скажем, вы допустили ошибку здесь (- вместо +)
  end

  def subtract(one, two)
  	one + two # и в этом методе
  end

  def divide(one, two)
  	one / two
  end
end

Тестируем:


require 'test/unit'
require_relative 'calculator.rb'

class CalculatorTest < Test::Unit::TestCase

  def setup
  	@calc = Calculator.new('test')
  end

  def test_addition
    assert_equal 4, @calc.add(2,2)
  end

  def test_subtraction
    assert_equal 2, @calc.subtract(4,2)
  end

  def test_division
    assert_equal 2, @calc.divide(4,2)
  end

end

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

Если исправить наши ошибки, тест будет полностью доволен.

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


require 'test/unit'
require_relative 'calculator.rb'

class CalculatorTest < Test::Unit::TestCase
  def setup
  	@calc = Calculator.new('test')
  end

  def test_divide_by_zero
    assert_raise ZeroDivisionError do 
      @calc.divide(1, 0)
    end
  end
end

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

Классы

Для начала мы поговорим о классах. Мы рассмотрим:

  • Классы
  • Как создаются объекты
  • Как получить доступ к данным в них

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

  • Определите вещи, с которыми ваша программа имеет дело
  • Этими вещами (их «чертежами») являются классы
    • Классы содержат методы (поведение)
  • Объекты являются экземплярами этих вещей
  • Объекты содержат переменные экземпляров (состояние)

Переменные экземпляров

В Ruby, переменные экземпляров начинаются на @. К примеру, @name.

Интересно то, что в Ruby переменные экземпляра не объявляются. Они как бы просто оживают, когда вызваны впервые.

Они доступны для всех методов экземпляра класса.

Создание объекта

Способ создания объектов в Ruby — вызов метода new в классе, фактически это вызывает метод initialize.

Метод initialize в Ruby — это специальный метод, который действует как конструктор, как мы увидим в примере ниже.

Состояние объекта должно быть инициализировано внутри метода initialize, то есть конструктора.


class Person
  def initialize (name, age) # "Конструктор"
    @name = name
    @age = age
  end
  def get_info
    @additional_info = "Интересно"
    "Имя: #{@name}, возраст: #{@age}"
  end
end

person1 = Person.new("Джо", 14)
p person1.instance_variables # [:@name, :@age]
puts person1.get_info # => Имя: Джо, возраст: 14
p person1.instance_variables # [:@name, :@age, :@additional_info]

Доступ к данным

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

Методы имеют публичный доступ по умолчанию.

Для доступа к переменным экземпляров нужно определить методы «getter» / «setter».


class Person 
  def initialize (name, age) # Конструктор
    @name = name 
    @age = age 
  end 
  def name # name getter
    @name 
  end 
  def name= (new_name) # name setter
    @name = new_name 
  end 
end 

person1 = Person.new("Джо", 14) 
puts person1.name # Джо
person1.name = "Майк" 
puts person1.name # Майк
# puts person1.age # undefined method `age' for #<Person:

Часто логика getter/setter довольно проста: получить существующее значение / установить новое значение.

Но должен быть путь еще проще, чем фактическое определение методов getter/setter.

Так что можно использовать синтаксис attr_*:

  • attr_accessor — getter и setter
  • attr_reader — только getter
  • attr_writer — только setter

class Person 
  attr_accessor :name, :age # геттеры и сеттеры для name и age
end 

person1 = Person.new 
p person1.name # => nil 
person1.name = "Майк" 
person1.age = 15 
puts person1.name # => Майк 
puts person1.age # => 15 
person1.age = "пятнадцать" 
puts person1.age # => пятнадцать

В этом примере есть две небольшие проблемы:

  • При создании, Person находится в неинициализированном состоянии (без name или age)
  • Скорее всего, мы хотели бы контролировать максимальный установленный возраст

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

Но для начала мы должны рассмотреть self.

self

Внутри метода экземпляра, self относится к самому объекту.

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

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

Вне определения метода экземпляра, self относится к самому классу.


class Person 
  attr_reader :age 
  attr_accessor :name 

  def initialize (name, ageVar) # Конструктор
    @name = name 
    self.age = ageVar # вызываем age= метод
    puts age 
  end 
  def age= (new_age) # более разумный setter для age
    @age = new_age unless new_age > 120
  end 
end 

person1 = Person.new("Ким", 13) # => 13 
puts "Мой возраст #{person1.age}" # => Мой возраст 13 
person1.age = 130 # Попытка изменить возраст
puts person1.age # => 13 (setter не дал провести изменение)

Далее мы поговорим о методах/переменных и наследовании классов.

Наследование классов

Мы рассмотрим:

  • Оператор ||
  • Методы классов и операторы классов
  • Наследование классов

Оператор ||

Оператор || оценивает левую сторону.

  • Если true — возвращает ее
  • В другом случае, возвращает правую сторону
  • @x = @x || 5 в первый раз возвратит 5, в следующий — @x

Краткая форма:

  • @x ||= 5 — то же, что и сверху

var = var || что-то

Давайте посмотрим чем это может быть полезно.


class Person 
  attr_reader :age 
  attr_accessor :name 
  
  def initialize (name, age) # Конструктор
    @name = name 
    self.age = age # вызывает age= метод
  end 
  def age= (new_age) 
    @age ||= 5 # устанавливаем 5 только для первого раза
    @age = new_age unless new_age > 120 
  end 
end 
person1 = Person.new("Ким", 130) 
puts person1.age # => 5 (по умолчанию) 
person1.age = 10 # изменяем на 10 
puts person1.age # => 10 
person1.age = 200 # Пытаемся изменить на 200
puts person1.age # => 10 (всё еще)

Методы и переменные классов

Давайте поговорим методах классов и переменных классов.

Они вызываются на классе, а не экземпляре класса. Так что если вы знакомы с концептом метода static в Java, это его эквивалент.

Self вне определения метода относится к объекту Class.

Как мы увидим, есть еще два способа определить методы класса в Ruby, кроме self вне определения метода.

Переменные класса начинаются на @@.

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


class MathFunctions 
  def self.double(var) # 1. Используем self 
    times_called; var * 2; 
  end 
  class << self # 2. Используем << self 
    def times_called 
      @@times_called ||= 0; @@times_called += 1 
    end 
  end 
end 
def MathFunctions.triple(var) # 3. Вне класса
  times_called; var * 3 
end

# Без создания экземпляра!
puts MathFunctions.double 5 # => 10 
puts MathFunctions.triple(3) # => 9 
puts MathFunctions.times_called # => 3 

Наследование класса

Каждый класс неявно наследуется от Object. Сам Object наследуется от BasicObject.

Множественного наследования нет, вместо этого используются примеси (mixins).

Давайте взглянем на небольшой пример.


class Dog # неявно наследует Object
  def to_s
    "Собака"
  end
  def bark
    "лает громко"
  end
end
class SmallDog < Dog # означает наследование, наследует класс Dog
  def bark # Перезаписываем
    "лает тихо"
  end
end

dog = Dog.new # (кстати, new это метод класса)
small_dog = SmallDog.new
puts "#{dog}1 #{dog.bark}" # => Собака1 лает громко
puts "#{small_dog}2 #{small_dog.bark}" # => Собака2 лает тихо

Модули

Теперь рассмотрим модули.

  • Модули
    • Как пространства имен
    • Как примеси
  • Использование встроенных в Ruby модулей, особенно Enumerable
  • require_relative

В общем-то, модули являются контейнерами для классов, методов и констант. Или других модулей.

Они походи на Классы, но не могут быть инстанцированы.
Class наследует Module и добавляет new.

Модули полезны для двух целей: пространства имен и примеси.

Модуль как пространство имен


module Sports
  class Match # Класс match как часть модуля Sports
    attr_accessor :score
  end
end

module Patterns
  class Match # Класс match как часть модуля Patterns
    attr_accessor :complete
  end
end

# Итак, мы имеем два разных Match

match1 = Sports::Match.new # заметьте использование оператора ::
match1.score = 40; puts match1.score # => 40

match2 = Patterns::Match.new
match2.complete = true; puts match2.complete # => true

Модуль как примеси

Другой способ использования модулей — это примеси.

В ОО-программировании есть интерфейсы. Они могут определять что класс «может» делать.

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

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


module SayMyName
  attr_accessor :name
  def print_name
    puts "Имя: #{@name}"
  end
end

class Person
  include SayMyName # позволяет включить функциональность этого модуля
end
class Company
  include SayMyName
end

person = Person.new
person.name = "Джо"
person.print_name # => Имя: Джо
company = Company.new
company.name = "Google & Microsoft LLC"
company.print_name # => Имя: Google & Microsoft LLC

Модуль Enumerable

Что действительно интересно, вы можете включить встроенные модули из самого Ruby. Когда мы говорили о массивах, мы затрагивали, что они имеют такие методы как map, select, reject, detect и т.д.

Дело в том, что массивы включают модуль Enumerable и это то что дает им такую функциональность.

Вы можете включить его в свой собственный класс.

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

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


# имя файла - player.rb

class Player

  attr_reader :name, :age, :skill_level

  def initialize (name, age, skill_level)
    @name = name
    @age = age
    @skill_level = skill_level
  end

  def to_s
    "<#{name}: #{skill_level}(SL), #{age}(возраст)>"
  end

end


# team.rb

class Team
  include Enumerable # Множество функций

  attr_accessor :name, :players
  def initialize (name)
    @name = name
    @players = []
  end
  def add_players (*players) # splat
    @players += players
  end
  def to_s
    "#{@name} team: #{@players.join(", ")}"
  end
  def each
    @players.each { |player| yield player }
  end
end


require_relative 'player' # require_relative позволяет включать другие .rb файлы
require_relative 'team'

player1 = Player.new("Боб", 13, 5); player2 = Player.new("Джим", 15, 4.5)
player3 = Player.new("Майк", 21, 5) ; player4 = Player.new("Джо", 14, 5)
player5 = Player.new("Скотт", 16, 3)

red_team = Team.new("Красная команда")
red_team.add_players(player1, player2, player3, player4, player5) # (splat)

# выбирает только игроков от 14 до 20 and отклоняет всех ниже уровня SL 4.5
elig_players = red_team.select {|player| (14..20) === player.age }
                       .reject {|player| player.skill_level < 4.5}
puts elig_players # => <Джим: 4.5(SL), 15(возраст)>
		  # => <Джо: 5(SL), 14(возраст)>

Области

Теперь рассмотрим области.

  • Области переменных
  • Области констант
  • Как область работает с блоками

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

Внешние переменные не имеют отношения к внутренней области.

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


v1 = "outside" # вне области

class MyClass 
  def my_method
  	# p v1 Выдается исключение - нет такой переменной
    v1 = "inside" # внутри
    p v1
    p local_variables 
  end 
end 

p v1 # => outside
obj = MyClass.new 
obj.my_method # => inside 
              # => [:v1] 
p local_variables # => [:v1, :obj]
p self # => main

Область: константы

  1. Константа — это любая ссылка, которая начинается с заглавной буквы, включая классы и модули.
  2. Правила области константы отличаются от правил области переменных.
  3. Внутренняя область может использовать константы, определенные вне этой области, и также может перезаписывать внешние константы.
    • При этом внешнее значение остается неизменным за ее пределами

Давайте взглянем на пример.


module Test
  PI = 3.14
  class Test2
    def what_is_pi
      puts PI
    end
  end
end
Test::Test2.new.what_is_pi # => 3.14

module MyModule
  MyConstant = 'Внешняя константа'
  class MyClass
    puts MyConstant # => Внешняя константа
    MyConstant = 'Внутренняя константа'
    puts MyConstant # => Внутренняя константа
  end

  # остается неизменной вне области
  puts MyConstant # => Внешняя константа
end

Область: блоки

Блоки, в отличие от методов, наследуют внешнюю область.

Блоки закрыты. Они запоминают контекст в котором они были определены и используют этот контекст при каждом вызове.

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


class BankAccount
  attr_accessor :id, :amount
  def initialize(id, amount)
    @id = id
    @amount = amount
  end
end

acct1 = BankAccount.new(123, 200) # id, сумма
acct2 = BankAccount.new(321, 100)
acct3 = BankAccount.new(421, -100)
accts = [acct1, acct2, acct3]

total_sum = 0 # объявляем переменную
accts.each do |eachAcct| # считаем общую сумму в блоке
  total_sum += eachAcct.amount
end

# та же область
puts total_sum # => 200

Блок – локальная область

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

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

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


arr = [5, 4, 1] 
cur_number = 10 
arr.each do |cur_number| 
  some_var = 10 # Не доступна вне блока
  print cur_number.to_s + " " # => 5 4 1 
end 
puts # отдает пустую строку
puts cur_number # => 10 

adjustment = 5 
arr.each do |cur_number;adjustment| 
  adjustment = 10 
  print "#{cur_number + adjustment} " # => 15 14 11 
end 
puts 
puts adjustment # => 5 (Не затронута блоком)

Контроль доступа

Теперь рассмотрим:

  • Три уровня контроля доступа
  • Управление доступом
  • Насколько приватен приватный доступ?

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

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

Всего Ruby предоставляет три метода контроля доступа: public, protected и private.

Инкапсуляция

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


class Car
  def initialize(speed, comfort)
    @rating = speed * comfort
  end

  # Нельзя задать rating извне
  def rating
    @rating
  end
end

puts Car.new(4, 5).rating # => 20

Назначение контроля доступа

В Ruby есть два пути назначения контроля доступа:

  1. Указать public, protected или private
    • Всё до следующего ключевого слова управления доступом будет этого уровня доступа
  2. Определить методы обычным методом и затем указать public, protected или private, и перечислить через запятую методы под этими уровнями, используя символы методов

По умолчанию (если вы не указываете ни один из уровней), методы являются public.

Итак, взглянем на пример такого назначения.


class MyAlgorithm
  private
    def test1
      "Приватный"
    end
  protected
    def test2
      "Защищенный"
    end
  public
    def public_again
      "Публичный"
    end
end

# Второй способ:

class Another
  def test1
    "Приватный, как объявлено ниже"
  end
  private :test1
end

Что означают публичный, защищенный и приватный уровни?

public методы — нет установленного управления доступом. Кто угодно может вызывать эти методы.

protected методы — могут быть вызваны объектами определяющего класса или его подклассами.

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

Приватный доступ


class Person
  def initialize(age)
  	self.age = age # Валидно - исключение
  	puts my_age
  	# puts self.my_age # Не валидно
  	                   # Нельзя использовать self на любом другом получателе
  end

  private 
    def my_age
    	@age
    end
    def age=(age)
      @age = age
    end
end

Person.new(24) # => 24

Строки

Для начала поговорим о строках. Мы рассмотрим:

  • Различные виды строк, поддерживаемые Ruby
  • Многие методы, поддерживаемые String API
  • Символы

Строки с одной кавычкой очень буквальны. Они позволяют экранирование ' с помощью /, и показывают всё остальное практически как есть.

Строки с двойными кавычками имеют специальные символы, которые они могут интерпретировать: /n, /t и другие.
Другая интересная функция, которую они имеют, это интерполяцию строк.

Строки / интерполяция


single_quoted = 'ice cream \n followed by it\'s a party!'
double_quoted = "ice cream \n followed by it\'s a party!" 

puts single_quoted # => ice cream \n followed by it's a party!
puts double_quoted # => ice cream 
                   # =>   followed by it's a party! 

def multiply (one, two) 
  "#{one} умножить на #{two} равняется #{one * two}" # интерполяция доступна только для строк с двойными кавычками
end 
puts multiply(7, 3) 
# => 7 умножить на 3 равняется 21

Еще о строках:

  • Строковые методы, заканчивающиеся на !, изменяют существующую строку
    • Большинство других просто возвращают новую строку
  • Можно использовать %Q{длинная мультистрочная строка}
    • Ведет себя так же как и строка с двойными кавычками

my_name = " тим" 
puts my_name.lstrip.capitalize # => Тим
# методы без ! выводят измененную копию строки
p my_name # => " тим" 

my_name.lstrip! # убирает первый пробел
# метод с ! на самом деле изменяет строку

my_name[0] = 'К' # заменяет первый символ
puts my_name # => Ким 

cur_weather = %Q{Сегодня жарко
			     Захватите зонтики...} 

cur_weather.lines do |line| 
  line.sub! 'жарко', 'дождливо' # заменяет "жарко" на "дождливо"
  puts "#{line.strip}"
end 
# => Сегодня дождливо
# => Захватите зонтики...

API строк

https://ruby-doc.org/core-2.4.1/String.html

Здесь есть много хороших примеров.

strings пример

strings пример 2

Символы

Другой тип строк, который многих удивляет, — это идея символа.

Итак, что такое символ? Символ — это двоеточие, за которым следует некоторая строка.

К примеру, :foo-. Символы — это просто оптимизированные строки.

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

Символы гарантированно будут уникальными и неизменными. Они могут быть конвертированы в Строку с помощью to_s, или со Строки в Символ с помощью to_sym.

Символ может являться репрезентацией имени метода.

Так, к примеру, на любом объекте в Ruby вы можете просто спросить, какие у меня методы? И он предоставит вам все методы, поддерживаемые этим объектом.

irb методы

В этом случае мы делаем чуть больше, мы ищем методы со словом case.

Интересно, что мы видим обычную версию метода и версию с !, которая изменяет строку безвозвратно.

Массивы

Далее мы обсудим массивы (множества):

  • Как они создаются
  • Как изменять массивы
  • Доступ к элементам внутри массива

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

Массивы:

  • Коллекция ссылок на объекты
  • Индексируется с использованием оператора []
  • Может индексироваться с отрицательными числами или диапазонами
  • В одном массиве разрешается иметь различные типы данных
  • Можно использовать %w{str1 str2} для создания массива строк

het_arr = [1, "два", :три] # различные типы
puts het_arr[1] # => два (индексы массива начинаются с 0)

arr_words = %w{ какой же сегодня хороший день! }
puts arr_words[-2] # => хороший
puts "#{arr_words.first} - #{arr_words.last}" # => какой - день! 
p arr_words[-3, 2] # => ["сегодня", "хороший"] (вернуться на 3 элемента назад и взять 2 из них) 

# (Диапазон, который мы рассмотрим позже...)
p arr_words[2..4] # => ["сегодня", "хороший", "день!"] 

# Сделать строку из элементов массива, разделенных ','
puts arr_words.join(',') # => какой,же,сегодня,хороший,день!

Изменение массивов:

  • Добавление: push или <<
  • Удаление: pop или shift
  • Задать конкретный элемент: метод []=

Другие функции:

  • Показать случайный элемент(ы) с помощью sample
  • Сортировать или реверсировать массив с помощью sort! и reverse!

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

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


# Вам нужен стек? Без проблем
stack = []; stack << "один"; stack.push ("два")
puts stack.pop # => два

# Вам нужна очередь? Тоже есть
queue = []; queue.push "один"; queue.push "два"
puts queue.shift # => один

a = [5,3,4,2].sort!.reverse! 
p a # => [5,4,3,2] (фактически изменяет массив) 
p a.sample(2) # => 2 случайных элемента

# Что будет, если добавить 33 как шестой (седьмой, считая с 0) элемент?
# Массив автоматически расширится и добавит nil вместо пустых элементов
a[6] = 33
p a # => [5, 4, 3, 2, nil, nil, 33]

Другие полезные методы:

  • each — перебирает массив
  • select — фильтрует массив, выбирая
  • reject — фильтрует массив, отклоняя
  • map — изменяет каждый элемент в массиве

API массивов.
Много других полезных методов можно найти, посмотрев API массивов: https://ruby-doc.org/core-2.4.1/Array.html

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


a = [1, 3, 4, 7, 8, 10]
a.each { |num| print num } # => 1347810 (нет новой строки)
puts # => (с новой строки)

new_arr = a.select { |num| num > 4 }
p new_arr # => [7, 8, 10]
new_arr = a.select { |num| num < 10 }
           .reject{ |num| num.even? }
p new_arr # => [1, 3, 7]

# Умножить каждый элемент на 3, создав новый массив
new_arr = a.map {|x| x * 3}
p new_arr # => [3, 9, 12, 21, 24, 30]

В итоге.
Таким образом, API массивов очень гибкий и очень мощный. Я просто показал несколько методов, но есть еще многие, которые могут быть полезными. И есть много способов обработки элементов.

Далее мы собираемся рассмотреть диапазоны.

Диапазоны

Поговорим о диапазонах и о том как они полезны в Ruby.

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

  • 1..20, 'a' .. 'z'

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

К примеру, в 1..10 включается 1, включается 10 и, очевидно, всё что между ними.

Если используется три точки, к примеру 1...10, то последняя цифра исключается. То есть, в этом примере будет включено 1, включено 1-9, но 10 исключено.

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

Что такого особенного в диапазонах? Разве мы не можем сделать это с помощью массивов?

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

Кроме того:

  • Диапазоны очень эффективны
  • Могут быть конвертированы в массив с помощью to_a
  • Используются для условий и интервалов

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

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

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


some_range = 1..3
puts some_range.max # => 3
puts some_range.include? 2 # => true

puts (1...10) === 5.3 # => true
puts ('a'...'r') === "r" # => false (конец исключается)

p ('k'..'z').to_a.sample(2) # => ["k", "w"]
# или другой случайный массив с двумя буквами из диапазона

age = 55
case age
  when 0..12 then puts "Еще ребенок"
  when 13..99 then puts "Молод в душе!"
  else puts "Вы становитесь старше..."
end
# => Молод в душе!

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

Хэши

Теперь поговорим о хэшах.

Как они используются, зачем они используются, и есть ли у них схожести с блоками.

Хэши:

  • Индексированные коллекции ссылок на объекты
  • Создаются с помощью {} или Hash.new
  • Также известны как ассоциативные массивы
  • Индекс (ключ) может быть чем угодно
    • А не только целым числом, как в случае с массивами

Также:

  • Доступны с помощью оператора []
  • Значения задаются с помощью:
    • => (создание)
    • [] (после создания)

Давайте взглянем на пример.


editor_props = { "font" => "Arial", "size" => 12, "color" => "red"}

# Выше указан не блок, это хэш
puts editor_props.length # => 3
puts editor_props["font"] # => Arial

editor_props["background"] = "Blue"
editor_props.each_pair do |key, value|
  puts "Ключ: #{key}, значение: #{value}"
end
# => Ключ: font, value: Arial
# => Ключ: size, value: 12
# => Ключ: color, value: red
# => Ключ: background, value: Blue

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

Если хэш был создан с помощью Hash.new(0) (0 в качестве примера), то будет возвращен 0.


API хэшей.
API хэшей тоже полезно освоить: https://ruby-doc.org/core-2.4.1/Hash.html

К примеру, используя синтаксис Hash.new со значением 0, мы хотим рассчитать частоту слов. Скажем, у нас есть предложение с какими-то словами, каждое из которых повторяется дважды.


word_frequency = Hash.new(0)

sentence = "Chicka chicka boom boom"
sentence.split.each do |word|
  word_frequency[word.downcase] += 1
end

p word_frequency # => {"chicka" => 2, "boom" => 2}

Еще о хэшах

В текущей версии Ruby, порядок записей в хэшах сохраняется.

При использовании символов в качестве ключей, можно использовать синтаксис symbol:.

Если хэш является последним аргументом метода, {} являются опциональными.

Давайте взглянем на всё это в примере.


family_tree_19 = {самый_старший: "Джим", старший: "Джо", младший: "Джек"} # хэш
family_tree_19[:самый_младший] = "Джереми"
p family_tree_19
# => {:самый_старший=>"Джим", :старший=>"Джо", :младший=>"Джек“, :самый_младший => “Джереми”}
# порядок поддерживается


def adjust_colors (props = {foreground: "красный", background: "белый"})
  puts "Передний план: #{props[:foreground]}" if props[:foreground]
  puts "Фон: #{props[:background]}" if props[:background]
end
adjust_colors # => Передний план: красный
              # => Фон: белый
adjust_colors ({ :foreground => "зеленый" }) # => Передний план: зеленый
adjust_colors background: "желтый" # => Фон: желтый
adjust_colors :background => "фиолетовый" # => Фон: фиолетовый

Путаница между хэшами и блоками

Это происходит не слишком часто, но иногда начинающие разработчики допускают ошибку.


# Скажем, у вас есть хэш
a_hash = { :one => "one" }

# Затем вы выводите его
puts a_hash # => {:one=>"one"}

# Если вы попытаетесь сделать это за один шаг - вы получите SyntaxError
# puts { :one => "one" }

# Ruby путается и думает, что {} является блоком!!!

# Чтобы обойти это, вы можете использовать скобки
puts ({ :one => "one" }) # => {:one=>"one"}

# Или вообще отбросить {}... 
puts one: "one" # => {:one=>"one"}

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

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

В этом посте мы обсудим:

  • Чтение и запись в файлы
  • Исключения
  • Чтение значений из переменных среды

Чтение из файла

Итак, у вас есть файл с именем test.txt, и вы хотите написать программу на Ruby, которая читает что-то из этого файла.

тестовый файл test.txt

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


File.foreach ( 'test.txt' ) do |line|
  puts line
  p line
  p line.chomp # отсекает символ новой строки
  p line.split # массив слов строки
end

чтение файла

Чтение неизвестного файла

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


File.foreach( 'do_not_exist.txt' do |line|
  puts line.chomp
end

Руби попытается прочитать файл, он не найдет его, и выдаст, что do_not_exist.txt не существует, и всё.

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

Чтобы обойти это, вы можете обработать исключение. В этом случае вы контролируете, что с этим делать.


begin

  File.foreach ( 'do_not_exist.txt' ) do |line|
    puts line.chomp
  end

rescue Exception => e
  puts e.message
  puts "Давайте притворимся, что этого не было"
end

Результат будет таким:
обработка исключений

Альтернатива исключениям

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


if File.exist? 'test.txt'

  File.foreach( 'test.txt' ) do |line|
    puts line.chomp
  end

end

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

Запись в файл

Как насчет записи в файл? Для этого Ruby тоже использует структуру блока.


File.open("test1.txt", "w") do |file|
  file.puts "Первая строка"
  file.puts "Вторая строка"
end

Файл будет автоматически закрыт после выполнения блока.

Переменные среды

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

К примеру, установив у себя переменную BEST_LANG, я могу прочесть ее:


puts ENV["BEST_LANG"] # => Ruby

В этом посте мы рассмотрим блоки в Ruby.

  • Блоки
  • Как они используются
  • Как включить их в свои собственные методы

Некоторые люди отбрасываются блоки в Ruby, потому что в других языках нет блоков. Но просто подумайте о них как о кусках кода, который выполняется. Вы передаете их между фигурными скобками ({}) или ключевыми словами do и end, и они передаются методам в качестве последнего параметра.

Конвенция:

  • Используйте {}, когда содержимое блока является одной строкой
  • Используйте do и end, когда содержимое блока занимает несколько строк

Блоки часто используются в качестве итераторов. Также они могут принимать аргументы.


1.times { puts "Привет, мир!" }
# => "Привет, мир!"

2.times do |index|
  if index > 0
    puts index
  end
end
# => 1

2.times { |index| puts index if index > 0 }
# => 1

Программирование с блоками

Есть два пути, чтобы настроить блок в вашем собственном методе.

Неявный:

  • Используйте block_given?, чтобы посмотреть был ли задан блок
  • Используйте yield, чтобы «вызвать» блок

Явный:

  • Используйте & перед последним «параметром»
  • Используйте метод call, чтобы вызвать блок

Неявный

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


def two_times_implicit
  return "Нет блока" unless block_given?
  yield
  yield
end

puts two_times_implicit { print "Привет " } # => "Привет Привет"
puts two_times_implicit # => Нет блока

Явный

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

Здесь вы должны проверить является ли блок nil?.


def two_times_explicit (&i_am_a_block)
  return "Нет блока" if i_am_a_block.nil?
  i_am_a_block.call
  i_am_a_block.call
end

puts two_times_explicit # => Нет блока
two_times_explicit { puts "Привет" } # => Привет
                                     # => Привет

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

В итоге

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

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