Построить класс ряд лампочек который хранит состояние ряда из 8 лампочек
Перейти к содержимому

Построить класс ряд лампочек который хранит состояние ряда из 8 лампочек

Объектно-ориентированное программирование. Язык Python

Зачем нужно что-то новое?
Главная проблема – сложность!
• программы из миллионов строк
• тысячи переменных и массивов
Э. Дейкстра: «Человечество еще в древности
придумало способ управления сложными системами:
«разделяй и властвуй»».
Структурное программирование:
декомпозиция по
задача
задачам
подзадача 1
подзадача 2.1
подзадача 2
подзадача 2.2
подзадача 3
подзадача 2.3
человек мыслит
иначе, объектами
2

3.

Как мы воспринимаем объекты?
существенные
свойства
Абстракция – это выделение существенных свойств объекта,
отличающих его от других объектов.
!
Разные цели –
разные модели!
3

4.

Использование объектов
Программа – множество объектов (моделей), каждый из
которых обладает своими свойствами и поведением, но его
внутреннее устройство скрыто от других объектов.
!
Нужно «разделить» задачу на объекты!
А
В
Б
Б1
В1
Б2
В2
В3
Б3
Г
Г1
Г2
декомпозиция по
объектам
4

5.

С чего начать?
Объектно-ориентированный анализ (ООА):
• выделить объекты
• определить их существенные свойства
• описать поведение (команды, которые они
могут выполнять)
Что такое объект?
?
Объектом можно назвать то, что имеет чёткие границы и
обладает состоянием и поведением.
Состояние определяет поведение:
• лежачий человек не прыгнет
• незаряженное ружье не выстрелит
Класс – это множество объектов, имеющих общую структуру
и общее поведение.
5

6.

Модель дороги с автомобилями
Объект «Дорога»:
ширина
(число полос)
длина
свойства
(состояние)
Дорога
длина
ширина
название
класса
методы
(поведение)
6

7.

Модель дороги с автомобилями
Объект «Машина»:
свойства: координаты и скорость
P
V
X
• все машины одинаковы
• скорость постоянна
• на каждой полосе – одна машина
• если машина выходит за правую
границу дороги, вместо нее слева
появляется новая машина
Машина
X (координата)
P (полоса)
V (скорость)
двигаться
Метод – это процедура или функция, принадлежащая классу
объектов.
7

8.

Модель дороги с автомобилями
Взаимодействие объектов:
Дорога
длина
ширина
узнать длину
Машина
X (координата)
P (полоса)
V (скорость)
двигаться
Схема определяет
• свойства объектов
• методы: операции, которые они могут выполнять
• связи (обмен данными) между объектами
!
Ни слова о внутреннем устройстве объектов!
8

9.

Классы
• программа – множество взаимодействующих объектов
• любой объект – экземпляр какого-то класса
• класс – описание группы объектов с общей структурой и
поведением
отличие от
структур!
Класс
Данные
состояние
Методы
поведение
Поле – это переменная, принадлежащая объекту.
9

10.

Класс «Дорога»
Описание класса:
class TRoad:
pass
Создание объекта:
road = TRoad()
!
Объекты-экземпляры не
создаются!
вызов конструктора
Конструктор – это метод класса, который вызывается для
создания объекта этого класса.
!
Конструктор по умолчанию строится автоматически!
10

11.

Новый конструктор – добавлений
полей
initialization – начальные
установки
ссылка для
class TRoad:
обращения к
def __init__ ( self ): самому объекту
self.length = 0
self.width = 0
оба поля
обнуляются
точечная запись
!
Конструктор задаёт начальные
значения полей!
road = TRoad()
road.length = 60
road.width = 3
изменение
значений
полей
11

12.

Конструктор с параметрами
автоматически
class TRoad:
def __init__ ( self
self, length0, width0 ):
self.length = length0
self.width = width0
Вызов:
road = TRoad( 60, 3 )
!
Нет защиты от неверных входных данных!
12

13.

Защита от неверных данных
class TRoad:
def __init__ ( self, length0, width0 ):
if length0 > 0:
self.length = length0
else:
self.length = 0
if width0 > 0:
self.width = width0
else:
self.width = 0
self.length = length0 if length0 > 0 else 0
self.width = width0 if width0 > 0 else 0
13

14.

Класс «Машина»
дорога, по
которой едет
полоса
class TCar:
def __init__ ( self, road0, p0, v0 ):
self.road = road0
скорость
self.P = p0
self.V = v0
координата
self.X = 0
14

15.

Класс «Машина» – метод move
class TCar:
def __init__ ( self, road0, p0, v0 ):
.
def move ( self ):
перемещение за t = 1
self.X += self.V
если за
if self.X > self.road.length:
пределами
self.X = 0
дороги
Равномерное движение:
X X 0 V t
t 1 интервал
дискретизации
перемещение за одну
единицу времени
15

16.

Основная программа
road = TRoad( 65, 3 )
car = TCar( road, 1, 10 )
car.move()
print ( «После 1 шага:» )
print ( car.X )
for i in range(10):
car.move()
print ( car.X )
class TCar:
.
?
Что выведет?
10
дошли до
конца дороги
def move ( self ):
self.X += self.V
if self.X > self.road.length:
self.X = 0
10
20
30
40
50
60
0
10
20
30
40
16

17.

Массив машин
N=3
cars = []
for i in range(N):
cars.append ( TCar(road, i+1, 2*(i+1)) )
for k in range(100):
for i in range(N):
cars[i].move()
# 100 шагов
# для каждой машины
print ( «После 100 шагов:» )
for i in range(N):
print ( cars[i].X )
17

18.

Что в этом хорошего и плохого?
ООП – это метод разработки больших программ!
основная программа – простая и понятная
классы могут разрабатывать разные программисты
независимо друг от друга (+интерфейс!)
повторное использование классов
неэффективно для небольших задач
18

19.

Задание
«A»: Построить класс Попугай (Parrot), который умеет
говорить какую-то фразу, заранее определённую при
описании класса.
Пример:
p = Parrot()
p.say()
Привет, друзья!
«B»: Изменить класс из задания A так, чтобы фраза
задавалась при создании конкретного экземпляра.
Пример:
p1 = Parrot( «Гав!» )
p2 = Parrot( «Мяу!» )
p1.say()
Гав!
p2.say()
Мяу!
19

20.

Задание
«С»: Изменить класс из задания B так, чтобы фразу можно
было изменять во время работы программы.
Пример:
p = Parrot( «Гав!» )
p.say()
Гав!
p.newText( «Мяу!» )
p.say()
Мяу!
«D»: Изменить класс из задания C так, чтобы при вызове
метода say можно было задать число повторений.
Пример:
p = Parrot( «Гав!» )
p.say()
Гав!
p.newText( «Мяу!» )
p.say( 3 )
Мяу! Мяу! Мяу!
20

21.

Задание
«E»: Изменить класс из задания D так, чтобы можно было
добавлять фразы в набор фраз, которые знает попугай.
При вызове метода say попугай выдаёт случайную фразу
из своего набора.
Пример:
p = Parrot( «Гав!» )
p.say()
Гав!
p.learn( «Мяу!» )
p.say()
Гав!
p.say(3)
Мяу! Мяу! Мяу!
21

22.

Зачем скрывать внутреннее
устройство?
интерфейсы
?
Объектная модель задачи:
?
защита внутренних данных
проверка входных данных на корректность
изменение устройства с сохранением интерфейса
Инкапсуляция («помещение в капсулу») – скрытие
внутреннего устройства объектов.
!
Также объединение данных и методов в
одном объекте!
22

23.

Защита внутренних данных
состояние
методы
!
Cat
энергия
настроение
голод
есть
спать
играть
Меняем состояние
только через методы!
?
Можно изменять
вучную?
метод есть
+ энергия
+ настроение
— голод
метод спать
+ энергия
+ голод
метод играть
— энергия
+ настроение
+ голод
23

24.

Пример: класс «перо»
class TPen:
def __init__ ( self ):
self.color = «000000»
!
R
G
B
По умолчанию все члены класса открытые (в других
языках – public)!
class TPen:
def __init__ ( self ):
__color = «000000»
self.__color
!
?
Как обращаться к
полю?
Имена скрытых полей (private) начинаются с двух
знаков подчёркивания!
24

25.

Пример: класс «перо»
class TPen:
def __init__ ( self ):
self.__color = «000000»
метод чтения
def getColor ( self ):
return self.__color
метод
записи
def setColor ( self, newColor ):
if len(newColor) != 6:
self.__color = «000000»
если ошибка,
else:
чёрный цвет
self.__color = newColor
!
Защита от неверных данных!
25

26.

Пример: класс «перо»
Использование:
установить
цвет
pen = TPen()
pen.setColor ( «FFFF00» )
print ( «цвет пера:», pen.getColor() )
!
Не очень удобно!
прочитать
цвет
pen.color = «FFFF00»
print ( «цвет пера:», pen.сolor )
26

27.

Свойство color
Свойство – это способ доступа к внутреннему
состоянию объекта, имитирующий обращение к его
внутренней переменной.
class TPen:
def __init__ ( self ):
.
def __getColor ( self ):
.
def __setColor ( self, newColor ):
.
метод чтения
color = property ( __getColor,
__setColor ) метод записи
свойство
pen.color = «FFFF00»
print ( «цвет пера:», pen.сolor )
27

28.

Изменение внутреннего устройства
Удобнее хранить цвет в виде числа:
class TPen:
def __init__ ( self ):
число
self.__color = 0
def __getColor ( self ):
return «».format ( self.__color )
def __setColor ( self, newColor ):
if len(newColor) != 6:
число
self.__color = 0
число
else:
self.__color = int ( newColor, 16 )
color = property (__getColor, __setColor)
!
Интерфейс не изменился!
28

29.

Преобразование int hex
Целое – в шестнадцатеричную запись:
16711935 «FF00FF»
x = 16711935
sHex = «».format(x)
?
Что плохо?
в шестнадцатеричной
системе
255 «FF»
«0000FF»
правильно так!
x = 16711935
sHex = «».format(x)
дополнить
нулями
слева
занять 6
позиций
29

30.

Преобразование hex int
«FF00FF»
16711935
sHex = «FF00FF»
x = int ( sHex, 16 )
система
счисления
30

31.

Свойство «только для чтения»
Скорость машины можно только читать:
class TCar:
def __init__ ( self ):
self.__v = 0
v = property ( lambda x: x.__v )
нет метода записи
31

32.

Скрытие внутреннего устройства
Инкапсуляция («помещение в капсулу»)
свойства
внутреннее
устройство
(private)
методы
интерфейс
(public)
32

33.

Задание
«A»: Построить класс РядЛампочек (LampRow), который
хранит состояние ряда из 8 лампочек в виде символьной
строки. Цифра 0 обозначает выключенную лампочку,
цифра 1 – включенную.
Свойство state скрывает внутреннюю переменную
__state, которая хранит состояние лампочек. При записи
нового значения проверяется, что длина строки
состояния равна 8, иначе записываются все нули.
Метод show выводит на экран состояние лампочек,
обозначая выключенную лампочку как минус, а
включённую – как «*».
Пример:
lamps = LampRow()
lamps.show()
——-lamps.state = «10101010»
print( lamps.state )
10101010
lamps.show()
*-*-*-*33

34.

Задание
«B»: Дополните класс LampRow из задания A так, чтобы
количество лампочек в цепочке можно было задавать в
конструкторе.
Пример:
lamps = LampRow( 6 )
lamps.show()
——lamps.state = «101010»
print( lamps.state )
101010
lamps.show()
*-*-*lamps.state = «10101010»
# ошибка
print( lamps.state )
000000
lamps.show()
——
34

35.

Задание
«С»: Дополните класс LampRow из задания B так, чтобы
лампочки могли гореть одним из двух цветов – красный
цвет имеет код 1 и обозначается при выводе как «*», а
зелёный цвет имеет код 2 и обозначается как «о».
Пример:
lamps = LampRow( 6 )
lamps.show()
——lamps.state = «102102»
print( lamps.state )
102102
lamps.show()
*-o*-o
lamps.state = «10201010»
# ошибка
print( lamps.state )
000000
lamps.show()
——
35

36.

Задание
«D»: Дополните класс LampRow из задания C так, чтобы код
состояния хранился как целое число. При этом
интерфейс (способ чтения и записи свойства state) не
должен измениться.
Пример:
lamps = LampRow( 6 )
lamps.show()
——lamps.state = «102102»
print( lamps.state )
102102
lamps.show()
*-o*-o*
lamps.state = «10201010»
# ошибка
print( lamps.state )
000000
lamps.show()
——
36

37.

Классификации
?
Что такое классификация?
Классификация – разделение изучаемых объектов на
группы (классы), объединенные общими признаками.
?
Зачем это нужно?
Фрукт
Яблоко
Груша
Банан
базовый класс
Апельсин
классынаследники
это фрукт,
у которого…
37

38.

Что такое наследование?
класс Двудольные
семейство Бобовые
род Клевер
горный клевер
наследует свойства
(имеет все свойства)
Класс Б является наследником класса А, если можно
сказать, что Б – это разновидность А.
яблоко – фрукт
яблоко – это фрукт
горный клевер – клевер
горный клевер – это
растение рода Клевер
машина – двигатель
машина содержит двигатель
(часть – целое)
38

39.

Иерархия логических элементов
Логический элемент
с одним входом
НЕ
с двумя входами
И
ИЛИ
Объектно-ориентированное программирование – это такой
подход к программированию, при котором программа
представляет собой множество взаимодействующих
объектов, каждый из которых является экземпляром
определенного класса, а классы образуют иерархию
наследования.
39

40.

Базовый класс
ЛогЭлемент
In1 (вход 1)
In2 (вход 2)
Res (результат)
calc
?
class TLogElement:
def __init__ ( self ):
self.__in1 = False
self.__in2 = False
self._res = False
Зачем хранить результат?
поле доступно
наследникам!
можно моделировать элементы с
памятью (триггеры)
40

41.

Базовый класс
class TLogElement:
def __init__( self ):
self.__in1 = False
self.__in2 = False
self._res = False
def __setIn1 ( self, newIn1 ):
self.__in1 = newIn1
пересчёт выхода
self.calc()
def __setIn2 ( self, newIn2 ):
self.__in2 = newIn2
self.calc()
In1 = property (lambda x: x.__in1, __setIn1)
In2 = property (lambda x: x.__in2, __setIn2)
Res = property (lambda x: x._res )
только для
чтения
41

42.

Метод calc
Как написать метод calc?
class TLogElement:
.
def calc ( self ):
pass
!
заглушка
Нужно запретить создавать объекты TLogElement!
42

43.

Абстрактный класс
• все логические элементы должны иметь метод calc
• метод calc невозможно написать, пока неизвестен тип
логического элемента
Абстрактный метод – это метод класса, который объявляется,
но не реализуется в классе.
Абстрактный класс – это класс, содержащий хотя бы один
абстрактный метод.
нет логического элемента «вообще», как не «фрукта
вообще», есть конкретные виды
!
Нельзя создать объект абстрактного класса!
TLogElement – абстрактный класс из-за метода calc
43

44.

Абстрактный класс
class TLogElement:
def __init__ ( self ):
self.__in1 = False
если у объекта нет атрибута
self.__in2 = False
(поля или метода) с именем
calc…
self._res = False
if not hasattr ( self, «calc» ):
raise NotImplementedError(
«Нельзя создать такой объект!»)
создать («поднять»,
«выбросить»)
исключение
44

45.

Что такое полиморфизм?
class TLogElement:
def __init__( self ):
.
def __setIn1 ( self, newIn1 ):
self.__in1 = newIn1
self.calc()
для каждого наследника
вызывается свой метод
calc
Полиморфизм – это возможность классов-наследников поразному реализовать метод с одним и тем же именем.
греч.: πολυ — много, μορφη — форма
45

46.

Элемент «НЕ»
наследник от
TLogElement
вызов
конструктора
базового класса
class TNot ( TLogElement ):
def __init__ ( self ):
TLogElement.__init__ ( self )
def calc ( self ):
self._res = not self.In1
?
!
Почему не __in1?
Это уже не абстрактный класс!
46

47.

Элемент «НЕ»
Использование:
n = TNot()
создание объекта
n.In1 = False
установка входа
print ( n.Res )
вывод результата
47

48.

Элементы с двумя входами
наследник от
TLogElement
class TLog2In ( TLogElement ):
pass
?
Можно ли создать объект этого класса?
нельзя, он абстрактный
48

49.

Элементы с двумя входами
Элемент «И»:
class TAnd ( TLog2In ):
def __init__ ( self ):
TLog2In.__init__ ( self )
def calc ( self ):
self._res = self.In1 and self.In2
Элемент «ИЛИ»:
class TOr ( TLog2In ):
def __init__ ( self ):
TLog2In.__init__ ( self )
def calc ( self ):
self._res = self.In1 or self.In2
49

50.

Пример: элемент «И-НЕ»
elNot = TNot()
elAnd = TAnd()
print ( » A | B | not(A&B) » );
print ( «——————-» );
for A in range(2):
A
elAnd.In1 = bool(A)
&
for B in range(2):
B
elAnd.In2 = bool(B)
elNot.In1 = elAnd.Res
print ( » «, A, «|», B, «|»,
int(elNot.Res) )
50

51.

Модульность
Идея: выделить классы в отдельный модуль
logelement.py.
class TLogElement:
.
class TNot ( TlogElement ):
.
class TLog2In ( TLogElement ):
pass
class TAnd ( TLog2In ):
.
class TOr ( TLog2In ):
.
51

52.

Модульность
В основную программу:
import logelement
elNot = logelement.TNot()
elAnd = logelement.TAnd()
.
52

53.

Сообщения между объектами
Задача – автоматическая передача
сигналов по цепочке!
class TLogElement:
def __init__ ( self ):
адрес следующего
элемента в цепочке
.
self.__nextEl = None
номер входа следующего
self.__nextIn = 0
элемента
.
def link ( self, nextEl, nextIn ):
self.__nextEl = nextEl
установка
self.__nextIn = nextIn
связи
53

54.

Сообщения между объектами
После изменения выхода «дергаем» следующий
элемент:
class TLogElement:
.
def __setIn1 ( self, newIn1 ):
self.__in1 = newIn1
если следующий элемент
self.calc()
установлен…
if self.__nextEl:
if self.__nextIn == 1:
self.__nextEl.In1 = self._res
elif __nextIn == 2:
__nextEl.In2 = self._res
передать результат на
нужный вход
54

55.

Сообщения между объектами
Изменения в основной программе:
elNot = TNot()
установить
elAnd = TAnd()
связь
elAnd.link ( elNot, 1 )
print ( » A | B | not(A&B) » );
print ( «——————-» );
for A in range(2):
elAnd.In1 = bool(A)
это уже не
for B in range(2):
нужно!
elAnd.In2 = bool(B)
elNot.In1 = elAnd.Res
print ( » «, A, «|», B, «|»,
int(elNot.Res) )
55

56.

Задание
«A»: Постройте класс Pet (домашнее животное) с двумя
скрытыми полями: __name (имя) и __age (возраст). Они
должны быть доступны для чтения через свойства name
и age и недоступны для записи. Метод gettingOlder
увеличивает возраст на 1 год. Класс Pet – абстрактный,
он имеет абстрактный метод say.
Постройте два класса-наследника – Cat (кошка) и Dog
(собака).Они должны реализовать метод say.
Описания классов должны быть в отдельном модуле
animals.py.
Пример: см. следующий слайд.
56

57.

Задание
«A»:
Шарик: 6 лет
Пример:
Мурка: Мяу!
from animals import *
p = Dog(«Шарик», 5)
Шарик: Гав!
p.gettingOlder()
print( p.name + «:», p.age, «лет»)
pets = [ Cat(«Мурка», 3), p ]
for p in pets:
p.say()
57

58.

Задание
«B»: Добавьте класс Mammal (млекопитающее) –
наследник класса Pet и предок для классов Cat и
Dog. Он должен иметь метод run (бежать), который
выводит сообщение вида «Вася побежал».
Пример:
from animals import *
pets = [Cat(«Мурзик», 3),
Dog(«Шарик», 5) ]
for p in pets:
Мурзик: Мяу!
p.say()
Мурзик побежал.
p.run()
Шарик: Гав!
Шарик побежал.
58

59.

Задание
«C»: Добавьте класс Reptilia (рептилии) – наследник класса
Pet и предок для новых классов Turtle (черепаха) и
Snake (змея). Он должен иметь метод crawl (ползти),
который выводит сообщение вида «Вася пополз…».
Пример:
from animals import *
Мурзик: Мяу!
pets = [Cat(«Мурзик», 3),
Мурзик побежал.
Turtle(«Зак», 32),
Зак: .
Dog(«Шарик», 5),
Зак пополз.
Snake(«Чаки», 2) ]
Шарик: Гав!
for p in pets:
Шарик побежал.
p.say()
Чаки: ш-ш-ш-ш.
if isinstance(p, Mammal):
Чаки пополз.
p.run()
if isinstance(p, Reptilia):
p.crawl()
59

60.

Задание
«A»: Собрать полную программу и построить таблицу
истинности последовательного соединения элементов
«ИЛИ» и «НЕ».
Пример:
A | B | not(A+B)
——————0 | 0 | 1
0 | 1 | 0
1 | 0 | 0
1 | 1 | 0
60

61.

Задание
«B»: Добавить в иерархию классов элементы «И-НЕ» (TNAnd)
и «ИЛИ-НЕ» (TNOr), которые представляют собой
последовательные соединения элементов «И» и «ИЛИ» с
элементом «НЕ». Построить их таблицы истинности.
Пример:
A | B | A nand B
——————0 | 0 | 1
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
A | B | A nor B
——————0 | 0 | 1
0 | 1 | 0
1 | 0 | 0
1 | 1 | 0
61

62.

Задание
«C»: Добавить в иерархию классов элемент «исключающее
ИЛИ» (TXor) и «импликация» (TImp). Построить их
таблицы истинности.
Пример:
A | B | A xor B
——————0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
A | B | A -> B
——————0 | 0 | 1
0 | 1 | 1
1 | 0 | 0
1 | 1 | 1
62

63.

Задание
«D»: Добавить в иерархию классов элемент «триггер»
(TTrigger). Построить его таблицу истинности при
начальных значениях выхода Q, равных 0 и 1.
Пример:
При Q = 0:
A | B | Q
——————0 | 0 | 0
0 | 1 | 0
1 | 0 | 1
1 | 1 | 1
При Q = 1:
A | B | Q
——————0 | 0 | 1
0 | 1 | 0
1 | 0 | 1
1 | 1 | 1
63

Помогите решить задачу в питоне

№1: Построить класс РядЛампочек (LampRow), который хранит состояние ряда из 8 лампочек в виде символьной строки. Цифра 0 обозначает выключенную лампочку, цифра 1 – включенную.

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

Метод show выводит на экран состояние лампочек, обозначая выключенную лампочку как минус, а включённую – как «*».

print( lamps.state ) 10101010

№2: Дополните класс LampRow из задания A так, чтобы количество лампочек в цепочке можно было задавать в конструкторе.

lamps = LampRow( 6 )

print( lamps.state ) 101010 lamps.show() *-*-*-

lamps.state = «10101010» # ошибка

print( lamps.state ) 000000 lamps.show() ——

№3: Дополните класс LampRow из задания B так, чтобы лампочки могли гореть одним из двух цветов – красный цвет имеет код 1 и обозначается при выводе как «*», а зелёный цвет имеет код 2 и обозначается как «о».

lamps = LampRow( 6 )

print( lamps.state ) 102102 lamps.show() *-o*-o

lamps.state = «10201010» # ошибка

print( lamps.state ) 000000 lamps.show() ——

Голосование за лучший ответ

class LampRow: 
def __init__(self):
self.__state = "0"*8

@property
def state(self):
return self.__state

@state.setter
def state(self, value):
if len(value) == 8:
self.__state = value
else:
self.__state = "0"*8

def show(self):
print("".join("-" if char == "0" else "*" for char in self.__state))
class LampRow: 
def __init__(self, num_lamps):
self.num_lamps = num_lamps
self.__state = "0"*num_lamps

@property
def state(self):
return self.__state

@state.setter
def state(self, value):
if len(value) == self.num_lamps:
self.__state = value
else:
self.__state = "0"*self.num_lamps
print("error: invalid state string length")

def show(self):
print("".join("-" if char == "0" else "*" for char in self.__state))
class LampRow: 
def __init__(self, num_lamps):
self.num_lamps = num_lamps
self.__state = "0"*num_lamps

@property
def state(self):
return self.__state

@state.setter
def state(self, value):
if len(value) == self.num_lamps:
self.__state = value
else:
self.__state = "0"*self.num_lamps
print("error: invalid state string length")

def show(self):
print("".join("*" if x == "1" else "o" if x == "2" else "-" for x in self.__state))

Вам действительно нужен Redux?

Не так давно React позиционировал себя как «V in MVC». После этого коммита маркетинговый текст изменился, но суть осталась той же: React отвечает за отображение, разработчик — за все остальное, то есть, говоря в терминах MVC, за Model и Controller.

Одним из решений для управления Model (состоянием) вашего приложения стал Redux. Его появление мотивировано возросшей сложностью frontend-приложений, с которой не способен справиться MVC.

Главный Технический Императив Разработки ПО — управление сложностью

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

  • состояние всего приложения хранится в одном месте
  • единственный способ изменить состояние — отправка Action’ов
  • все изменения происходят с помощью чистых функций

Смог ли Redux побороть возросшую сложность и было ли с чем бороться?

MVC не масштабируется

Redux вдохновлен Flux’ом — решением от Facebook. Причиной создания Flux, как заявляют разработчики Facebook (видео), была проблема масштабируемости архитектурного шаблона MVC.

По описанию Facebook, связи объектов в больших проектах, использующих MVC, в конечном итоге становятся непредсказуемыми:

  1. modelOne изменяет viewOne
  2. viewOne во время своего изменения изменяет modelTwo
  3. modelTwo во время своего изменения изменяет modelThree
  4. modelThree во время своего изменения изменяет viewTwo и viewFour

О проблеме непредсказуемости изменений в MVC также написано в мотивации Redux’a. Картинка ниже иллюстрирует как видят эту проблему разработчики Facebook’а.

Flux, в отличии от описанного MVC, предлагает понятную и стройную модель:

  1. View порождает Action
  2. Action попадает в Dispatcher
  3. Dispatcher обновляет Store
  4. Обновленный Store оповещает View об изменении
  5. View перерисовывается

Кроме того, используя Flux, несколько Views могут подписаться на интересующие их Stores и обновляться только тогда, когда в этих Stores что-нибудь изменится. Такой подход уменьшает количество зависимостей и упрощает разработку.

Реализация MVC от Facebook полностью отличается от оригинального MVC, который был широко распространен в Smalltalk-мире. Это отличие и является основной причиной заявления «MVC не масштабируется».

Назад в восьмидесятые

MVC — это основной подход к разработке пользовательских интерфейсов в Smalltalk-80. Как Flux и Redux, MVC создавался для уменьшения сложности ПО и ускорения разработки. Я приведу краткое описание основных принципов MVC-подхода, более детальный обзор можно почитать здесь и здесь.

  • Model — это центральная сущность, которая моделирует реальный мир и бизнес-логику, предоставляет информацию о своем состоянии, а также изменяет свое состояние по запросу из Controller’a
  • View получает информацию о состоянии Model и отображает ее пользователю
  • Controller отслеживает движение мыши, нажатие на кнопки мыши и клавиатуры и обрабатывает их, изменяя View или Model

А теперь то, что упустил Facebook, реализуя MVC — связи между этими сущностями:

  • View может быть связана только с одним Сontroller’ом
  • Сontroller может быть связан только с одним View
  • Model ничего не знает о View и Controller и не может их изменять
  • View и Controller подписываются на Model
  • Одна пара View и Controller’а может быть подписана только на одну Model
  • Model может иметь много подписчиков и оповещает всех их после изменения своего состояния

Посмотрите на изображение ниже. Стрелки, направленные от Model к Controller’у и View — это не попытки изменить их состояние, а оповещения об изменениях в Model.

Оригинальный MVC совершенно не похож на реализацию Facebook’a, в которой View может изменять множество Model, Model может изменять множество View, а Controller не образует тесную связь один-к-одному с View. Более того, Flux — это MVC, в котором роль Model играют Dispatcher и Store, а вместо вызова методов происходит отправка Action’ов.

React через призму MVC

Давайте посмотрим на код простого React-компонента:

class ExampleButton extends React.Component < render() < return (  ); > >

А теперь еще раз обратимся к описанию Controller’a в оригинальном MVC:

Controller отслеживает движение мыши, нажатие на кнопки мыши и клавиатуры и обрабатывает их, изменяя View или Model

Сontroller может быть связан только с одним View

Заметили, как Controller проник во View на третьей строке компонента? Вот он:

onClick= console.log("clicked!")>

Это идеальный Controller, который полностью удовлетворяет своему описанию. JavaScript сделал нашу жизнь легче, убрав необходимость самостоятельно отслеживать положение мыши и координаты в которых произошло нажатие. Наши React-компоненты превратились не просто во View, а в тесно связанные пары View-Controller.

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

Готовим MVC

Для удобства работы с React-компонентами, создадим свой класс BaseView, который будет подписываться на переданную в props Model:

// src/Base/BaseView.tsx import * as React from "react"; import BaseModel from "./BaseModel"; export default class extends React.Component, <>> < protected model: Model; constructor(props: any) < super(props); this.model = props.model >componentWillMount() < this.model.subscribe(this); >componentWillUnmount() < this.model.unsubscribe(this); >>

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

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

// src/Base/BaseModel.ts export default class < protected views: React.Component[] = []; subscribe(view: React.Component) < this.views.push(view); view.forceUpdate(); >unsubscribe(view: React.Component) < this.views = this.views.filter((item: React.Component) =>item !== view); > protected updateViews() < this.views.forEach((view: React.Component) =>view.forceUpdate()) > >

Я реализую всем известный TodoMVC с урезанным функционалом, весь код можно посмотреть на Github.

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

// src/TodoList/TodoListModel.ts import BaseModel from "../Base/BaseModel"; import TodoItemModel from "../TodoItem/TodoItemModel"; export default class extends BaseModel < private allItems: TodoItemModel[] = []; private mode: string = "all"; constructor(items: string[]) < super(); items.forEach((text: string) =>this.addTodo(text)); > addTodo(text: string) < this.allItems.push(new TodoItemModel(this.allItems.length, text, this)); this.updateViews(); >removeTodo(todo: TodoItemModel) < this.allItems = this.allItems.filter((item: TodoItemModel) =>item !== todo); this.updateViews(); > todoUpdated() < this.updateViews(); >showAll() < this.mode = "all"; this.updateViews(); >showOnlyActive() < this.mode = "active"; this.updateViews(); >showOnlyCompleted() < this.mode = "completed"; this.updateViews(); >get shownItems() < if (this.mode === "active") < return this.onlyActiveItems; >if (this.mode === "completed") < return this.onlyCompletedItems; >return this.allItems; > get onlyActiveItems() < return this.allItems.filter((item: TodoItemModel) =>item.isActive()); > get onlyCompletedItems() < return this.allItems.filter((item: TodoItemModel) =>item.isCompleted()); > >

Задача содержит в себе текст и идентификатор. Она может быть либо активной, либо выполненной, а также может быть удалена из списка. Выразим эти требования в модели:

// src/TodoItem/TodoItemModel.ts import BaseModel from "../Base/BaseModel"; import TodoListModel from "../TodoList/TodoListModel"; export default class extends BaseModel < private completed: boolean = false; private todoList?: TodoListModel; id: number; text: string = ""; constructor(id: number, text: string, todoList?: TodoListModel) < super(); this.id = id; this.text = text; this.todoList = todoList; >switchStatus() < this.completed = !this.completed this.todoList ? this.todoList.todoUpdated() : this.updateViews(); >isActive() < return !this.completed; >isCompleted() < return this.completed; >remove() < this.todoList && this.todoList.removeTodo(this) >>

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

// src/TodoList/TodoListInputView.tsx import * as React from "react"; import BaseView from "../Base/BaseView"; import TodoListModel from "./TodoListModel"; export default class extends BaseView> < render() < return (  < const enterPressed = e.which === 13; if (enterPressed) < this.model.addTodo(e.target.value); e.target.value = ""; >>> /> ); > >

Зайдя в такой View, мы сразу видим, как Controller (props onKeyDown) взаимодействует с Model и View, и какая конкретно Model используется. Нам не нужно отслеживать всю цепочку передачи props’ов от компонента к компоненту, что уменьшает когнитивную нагрузку.

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

И создадим View для отображения одной задачи, который будет работать с моделью TodoItemModel:

TodoMVC готов. Мы использовали только собственные абстракции, которые заняли меньше 60 строк кода. Мы работали в один момент времени с двумя движущимися частями: Model и View, что снизило когнитивную нагрузку. Мы также не столкнулись с проблемой отслеживания функций через props’ы, которая быстро превращается в ад. А еще нам не пришлось создавать фэйковые Container-компоненты.

Что не так с Redux?

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

  • уметь сохранять свое состояние в local storage и стартовать, используя сохраненное состояние
  • уметь заполнять свое состояние на сервере и передавать его клиенту внутри HTML
  • передавать Action’ы по сети
  • поддерживать undo состояния приложения

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

Redux слишком сложный, и я говорю не про количество строк кода в репозитории библиотеки, а про те подходы к разработке ПО, которые он проповедует. Redux возводит indirection в абсолют, предлагая начинать разработку приложения с одних лишь Presentation Components и передавать все, включая Action’ы для изменения State, через props. Большое количество indirection’ов в одном месте делает код сложным. А создание переиспользуемых и настраиваемых компонентов в начале разработки приводит к преждевременному обобщению, которое делает код еще более сложным для понимания и модификации.

Для демонстрации indirection’ов можно посмотреть на такой же TodoMVC, который расположен в официальном репозитории Redux. Какие изменения в State приложения произойдут при вызове callback’а onSave, и в каком случае они произойдут?

При отсутствии желания устраивать расследование самостоятельно, можно заглянуть под спойлер

  1. hadleSave из TodoItem передается как props onSave в TodoTextInput
  2. onSave вызывается при нажатии Enter или, если не передан props newTodo , на действие onBlur
  3. hadleSave вызывает props deleteTodo , если заметка изменилась на пустую строку, или props editTodo в ином случае
  4. props’ы deleteTodo и editTodo попадают в TodoItem из MainSection
  5. MainSection просто проксирует полученные props’ы deleteTodo и editTodo в TodoItem
  6. props’ы в MainSection попадают из контейнера App с помощью bindActionCreator , а значит являются диспетчеризацией action’ов из src/actions/index.js , которые обрабатываются в src/reducers/todos.js

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

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

Создание Flux и Redux было мотивировано немасштабируемостью MVC, но эта проблема исчезает, если применять оригинальный MVC. Redux пытается сделать изменение состояния приложения предсказуемым, но водопад из callback’ов в props’ах не только не способствует этому, но и приближает вас к потере контроля над вашим приложением. Возросшей сложности frontend-приложений, о которой говорят авторы Flux и Redux, не было. Было лишь неправильное использование подхода к разработке. Facebook сам создал проблему и сам же героически ее решил, объявив на весь мир о «новом» подходе к разработке. Большая часть frontend-сообщества последовала за Facebook, ведь мы привыкли доверять авторитетам. Но может настало время остановиться на мгновение, сделать глубокий вдох, отбросить хайп и дать оригинальному MVC еще один шанс?

UPD

Изменил изначальные view.setState(<>) на view.forceUpdate() . Спасибо, kahi4.

Типы данных, переменные

Переменная – это ячейка в оперативной памяти микроконтроллера, которая имеет своё уникальное название (а также адрес в памяти) и хранит значение соответственно своему размеру. К переменной мы можем обратиться по её имени или адресу и получить это значение, либо изменить его. Зачем это нужно? В переменной могут храниться п ромежуточные результаты вычислений, полученные “снаружи” данные (с датчиков, Интернета, интерфейсов связи) и так далее.

Измерение информации

Прежде чем перейти к переменным и их типам, нужно вспомнить школьный курс информатики, а именно – как хранятся данные в “цифровом” мире. Любая память состоит из элементарных ячеек, которые имеют всего два состояния: 0 и 1. Эта единица информации называется бит (bit). Минимальным блоком памяти, к которому можно обратиться из программы по имени или адресу, является байт (byte), который в Arduino (и в большинстве других платформ и процессоров) состоит из 8 бит, таким образом любой тип данных будет кратен 1 байту.

Максимальное количество значений, которое можно записать в один байт, составляет 2^8 = 256. В программировании счёт всегда начинается с нуля, поэтому один байт может хранить число от 0 до 255. Более подробно о двоичном представлении информации и битовых операциях мы поговорим в отдельном уроке.

Стандартные типы переменных в Arduino по своему размеру кратны степени двойки, давайте их распишем:

  • 1 байт = 8 бит = 256
  • 2 байта = 16 бит = 65 536
  • 4 байта = 32 бита = 4 294 967 296

Типы данных

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

Название Альт. название Вес Диапазон Особенность
boolean bool 1 байт * 0 или 1, true или false Логический тип
char 1 байт -128… 127 (AVR), 0.. 255 (esp) Символ (код символа) из таблицы ASCII
int8_t 1 байт -128… 127 Целые числа
byte uint8_t 1 байт 0… 255 Целые числа
int ** int16_t , short 2 байта -32 768… 32 767 Целые числа. На ESP8266/ESP32 – 4 байта! См. ниже
unsigned int ** uint16_t , word 2 байта 0… 65 535 Целые числа. На ESP8266/ESP32 – 4 байта! См. ниже
long int32_t 4 байта -2 147 483 648… 2 147 483 647 Целые числа
unsigned long uint32_t 4 байта 0… 4 294 967 295 Целые числа
float 4 байта -3.4E+38… 3.4E+38 Числа с плавающей точкой, точность: 6-7 знаков
double 4/8 байт -1.7E+308.. 1.7E+308 Для AVR то же самое, что float .
  • (*) – да, bool занимает 1 байт (8 бит), так как это минимальная адресуемая ячейка памяти. Есть способы запаковать логические переменные в 1 бит, о них поговорим в другом уроке.
  • (**) – на ESP8266/ESP32 int и unsigned int занимает 4 байта, то есть является аналогами типов long и unsigned long !
  • (***) – Компилятор также поддерживает 64 битные числа. Стандартные Arduino-библиотеки с переменными этого типа не работают, поэтому можно использовать только в своём коде.

Целочисленные типы

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

  • Проще ориентироваться в максимальных значениях
  • Легче запомнить
  • Название более короткое
  • Проще изменить один тип на другой
  • Размер переменной задан жёстко и не зависит от платформы (например int на AVR это 2 байта, а на esp8266 – 4 байта)

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

  • UINT8_MAX – 255
  • INT8_MAX – 127
  • UINT16_MAX – 65 535
  • INT16_MAX – 32 767
  • UINT32_MAX – 4 294 967 295
  • INT32_MAX – 2 147 483 647
  • UINT64_MAX – 18 446 744 073 709 551 615
  • INT64_MAX – ‭9 223 372 036 854 775 807

Логический тип

bool – логический, он же булевый (придуман Джорджем Булем) тип данных, принимает значения 0 и 1 или false и true – ложь и правда. Используется для хранения состояний, например включено/выключено, а также для работы в условных конструкциях.

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

bool a = 0; // false bool b = 1; // true bool c = 25; // true

Символьный тип

char – тип данных для хранения символов, символ указывается в одинарных кавычках: char var = ‘a’; . По факту это целочисленный тип данных, а переменная хранит номер (код) символа в таблице ASCII:

blank

Отдельный символьный тип данных нужен для удобства работы, чтобы программа могла понять разницу между числом и символом, например для вывода на дисплей (чтобы вывести именно букву A, а не число 65). Из символов можно составлять строки, об этом более подробно поговорим в уроках про символьные строки и String-строки.

Символы и числа

Несмотря на то, что в языке Си символ это по сути целое число, значения например ‘3’ и 3 не равны между собой, потому что символ ‘3’ с точки зрения программы является числом 51 . На практике иногда бывает нужно конвертировать символы чисел в соответствующие им целые числа и наоборот (при работе со строками и буферами вручную), для этого распространены следующие алгоритмы:

  • Из символа в число – взять младший ниббл (4 бита): symbol & 0xF
  • Из символа в число – вычесть символ 0: symbol — ‘0’
  • Из числа в символ – прибавить символ 0: symbol + ‘0’

Дробные числа

float (англ. float – плавающий) – тип данных для чисел с плавающей точкой, т.е. десятичных дробей. Arduino поддерживает три типа ввода чисел с плавающей точкой:

Тип записи Пример Чему равно
Десятичная дробь 20.5 20.5
Научный 2.34E5 2.34*10^5 или 234000
Инженерный 67e-12 67*10^-12 или 0.000000000067

Выше в таблице есть пометка “точность: 6-7 знаков” – это означает, что в этом типе можно хранить числа, размер которых не больше 6-7 цифр, остальные цифры будут утеряны! Причём целой части отдаётся приоритет. Вот так это выглядит в числах (в комментарии – реальное число, которое записалось в переменную):

float v; v = 123456.654321; // 123456.656250 v = 0.0123456789; // 0.0123456788 v = 0.0000123456789; // 0.0000123456788 v = 123456789; // 123456792.0

Другие особенности float чисел и работу с ними мы рассмотрим в уроках про математические операции и условия.

Объявление и инициализация

  • Объявление переменной – резервирование ячейки памяти указанного типа на имя: тип_данных имя;
  • Присваивание – задание переменной значения при помощи оператора = (равно): имя = значение;
  • Инициализация переменной – объявление и присваивание начального значения: тип_данных имя = значение;

Можно объявить и инициализировать несколько переменных через запятую:

byte myVal; int sensorRead = 10; byte val1, val2, val3 = 10;
  • Переменная должна быть объявлена до использования, буквально выше по коду. Иначе вы получите ошибку Not declared in this scope – переменная не объявлена.
  • Нельзя объявить две и более переменных с одинаковым именем в одной области определения.

Константы

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

Как переменную, указав перед типом данных слово const: const тип_данных имя = значение; . Пример: const byte myConst = 10; . По сути это будет обычная переменная, но её значение нельзя поменять. Особенности:

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

При помощи директивы #define, без знака равенства и точки с запятой в конце: #define имя значение . Пример: #define BTN_PIN 10 . Работает так: указанное имя буквально заменяется в тексте программы на указанное значение. Такая дефайн-константа:

  • Не занимает места в оперативной памяти, а хранится во Flash памяти как часть кода программы.
  • Не имеет адреса в оперативной памяти.
  • Вычисления с такими константами оптимизируются и выполняются быстрее, так как это просто цифры.
  • Если имя дефайн-константы совпадёт с именем другого “объекта” в программе или даже в библиотеке – работа может быть непредсказуемой: можно получить невнятную ошибку компиляции, либо программа может просто работать некорректно! Дефайн буквально заменяет текст в коде программы, это довольно опасная штука.

Во избежание проблем нужно называть дефайн-константы максимально уникальными именами. Можно добавлять к ним префиксы, например вместо PERIOD сделать MY_PERIOD и так далее.

Область видимости

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

Глобальная

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

byte var; // глобальная переменная void setup() < var = 50; >void loop()

Локальная

  • Объявляется внутри любого блока кода, заключённого в < фигурные скобки >.
  • Доступна для чтения и записи только внутри своего блока кода (и во всех вложенных в него).
  • Находится в оперативной памяти с момента объявления и до закрывающей фигурной скобки, то есть удаляется из памяти и её значение стирается.
  • При объявлении имеет случайное значение.

Важный момент: если имя локальной переменной совпадает с одной из глобальных, то приоритет обращения отдаётся локальной переменной (в её области определения).

byte var; // глобальная переменная void setup() < byte var; // локальная переменная var = 50; // меняем локальную var >void loop() < var = 70; // меняем глобальную var >

Статические переменные

Вспомним, как работает обычная локальная переменная: при входе в свой блок кода локальная переменная создаётся заново, а при выходе – удаляется из памяти и теряет своё значение. Если локальная переменная объявлена как static – она будет сохранять своё значение на всём протяжении работы программы, но область видимости останется локальной: взаимодействовать с переменной можно будет только внутри блока кода, где она создана (и во всех вложенных в него).

void setup() < >void loop() < byte varL = 0; varL++; static byte varS = 0; varS++; // здесь varL всегда будет равна 1 // а varS - постоянно увеличиваться >

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

Преобразование типов

Иногда требуется преобразовать один тип данных в другой: например, функция принимает int , а вы хотите передать ей byte . В большинстве случаев компилятор сам разберётся и преобразует byte в int , но иногда вылетает ошибка в стиле “попытка передать byte туда, где ждут int“. В таком случае можно преобразовать тип данных, для этого достаточно указать нужный тип данных в скобках перед преобразуемой переменной (тип_данных)переменная , иногда можно встретить запись тип_данных(переменная) . Результат вернёт переменную с новым типом данных, сам же тип данной у переменной не изменится. Например:

// переменная типа byte byte val = 10; // передаём какой-то функции, которая ожидает int sendVal( (int)val );

И всё! val будет обрабатываться как int , а не как byte .

Видео

Полезные страницы

  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

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

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