aleksey mashanov rit
TRANSCRIPT
Алексей Машанов
Поэтапный рефакторинг: success story
Цели рефакторинга● Упрощение добавления новых возможностей за счет
возможности реиспользования кода.
● Упрощение сопровождения кода за счет приведения его в человекочитаемый вид, нормализации кода и структуры базы.
● Избавление от велосипедов и перенос тем самым головной боли по их развитию и поддержке на сообщество.
Характеристики системы● Perl + PostgreSQL● ~ 1200 модулей и 400 скриптов● ~ 300000 строк чистого кода● ~ 450 таблиц в БД● ~ 150 хранимых процедур и 140
триггеров
Разбиение на этапы
t
Релиз (34 недели) Шаг рефакторинга — изменение не сказывающееся на работоспособности системы
Этап рефакторинга — должен укладываться в рамки одного релиза
коммит
Рефакторинг выполняется в основной ветке разработки
Test Driven RefactioringДля каждого вносимого в код изменения
Написание автотеста
Проверка автотеста путем поломки тестируемого кода
Внесение модификации (рефакторинг)
Проверка модифицированного кода автотестом
Структура автотестов
Class1
Class2
Class3
Class2::Sub1
Class2::Sub2
Class1::Test
Class2::Test
Class3::Test
Class2::Sub1::Test
Class2::Sub2::Test
Test::Class
My::Testlib/ t/lib/ rollback после каждого теста
I. Замена самописных ORMна DBIx::Class
Структура до рефакторингаEnt Ent::Smth11
Ent::Smth12
Ent::Smth1N
new()
get()
set()
list()
save()
Entity Entity::Smth11
Entity::Smth12
Entity::Smth1N
new()
get()
set()
list()
save()
… …
● Два примерно одинаковых ORM
● Методы модификации и поиска объединены в одном классе
● Доступ к полям объекта как к элементам хэша
Что хотим получитьDBIx::Class::Row Schema::Result::Smth11
Schema::Result::Smth1N
Schema::Result::Smth12
Schema::Result::Smth21
Schema::Result::Smth2N
Schema::Result::Smth22
DBIx::Class::ResultSet Schema::ResultSet::Smth11
Schema::ResultSet::Smth1N
Schema::ResultSet::Smth12
Schema::ResultSet::Smth21
Schema::ResultSet::Smth2N
Schema::ResultSet::Smth22
new()get_column()set_column()insert()update()
search()…
…
…
…
● Один ORM
● Методы модификации и поиска в разных классах
● Доступ к полям объекта через акцессоры
Зачем?● До рефакторинга
● Два самописных ORM в одной системе это слишком много
● Оба из них не поддерживают отношений между таблицами, тем не менее они нам необходимы, что приводит к обилию в коде plain SQL запросов
● Вновьприбывшие разработчики вынуждены с ними разбираться и вникать в их отличия
● После рефакторинга● Много новых хороших возможностей, которым мы все очень рады
● Мы не одни во вселенной: почти все что нам может понадобиться уже изобрели, реализовали, отладили и устранили почти все баги, а какие не устранили, устраняют довольно-таки быстро
● Опыт работы с DBIx::Class разработчику пригодится не только для работы над нашей системой, поэтому он с большей вероятностью потрудится разобраться в нем поглубже
Создание схемыtabletabletable
схемаDBIx::Class::Schema::Loader
code style conventionssimple perl script
Schema::Result::*выстраиваем нужную
иерархию
DBIx::Class::SchemaИспользуем статическую схему
Схема обертки
Ent::SmthN
dbic_class()Ent::Smth2
dbic_class()
Ent EntHash
Ent::Smth1
Schema::Result::SmthX
Schema::ResultSet::SmthX
tiehash
list()
search()
FETCH()
STORE()
EXISTS()
NEXTKEY()
get_coumn()
set_column()
has_column_loaded()
columns()
_DBIC_
_DBICRS_
insert_or_update()
new()
get()set()
save()
dbic_class()
Callback методы
Schema::Result::*
DBIC::EntCallback
insert()
update()
delete()
DBIx::Class::Core
caller() eq 'Ent'да
Ent::*
save()
delete()
нет
Неспешная миграцияEnt::XXX>... Schema>resultset('XXX')>...
SELECT * FROM xxx Schema>resultset('XXX')>select()
Ent::XXX>save() Schema::Result::XXX>insert()
Schema::Result::XXX>update()
Schema::Result::XXX>delete()Ent::XXX>delete()
1.
2.
3.
Завершение рефакторинга● Удаление иерархии старых ORM
Timeline рефакторинга
t
Схема таблиц Schema::*
Обертка Ent вокруг DBIx::Class
Callback методов
Замена Ent::* →Schema>resultset('*')
Избавление
от plain SQL
Перенос хуков в
Schema::Result::*
Удаление старого ORM
1 релиз
1 релиз
N релизов
II. Единый механизмхранения сущностей
Исходная структура
User Server VDS
Service
Lbill Client● Service связан с объектом одного из трех
классов, а не с одним.
● User, Server, VDS имеют примерно одинаковый набор финансовых полей, но не используют наследование.
● Лишняя связь от User, Server, VDS к Client.
● Сложные запросы к базе со множественными LEFT JOIN.
● Добавление новой сущности приводит к созданию 1 класса, 3 связей и модификации Service.
Желаемая структура
Client
Lbill
Entity
User
Server
VDSService
● Добавление новой сущности приводит к добавлению 1 класса и 1 связи.
● Финансовые операции ограничены работой с Entity, а не с тремя User, Server, VDS.
● При добавлении новой сущности большинство возможностей (кроме технических) - «из коробки».
● Нет лишних связей (нормализация).
Структура базыvz_vds
id
entity_id
технические поля
servers
id
entity_id
технические поля
vz_vds
lbill_id
client_idid
финансовые поля
технические поля
servers
lbill_id
client_idid
финансовые поля
технические поля
users
lbills
clients
Было Стало
entities
users
services services
entity_iduser_id
server_id
vz_vds_id
id
entity_id
технические поля
id
lbill_id
финансовые поля
id
client_id
id
lbill_id
client_idid
финансовые поля
технические поля
Миграционные триггеры
vz_vdsservers
entities
users
AFTER UPDATEОбновление соответствующих финансовых полей в таблицах users, servers, vz_vds при их изменении
BEFORE INSERT1.Проверка, что все синхронизируемые из entities поля IS NULL — это означает, что не выполняется попытка установить их значение при INSERT2.Автоматическое заполнение синхронизируемых полей данными из соответствующей записи в entities
Проверка, что все значения полей соответствуют значениям всех соответствующих полей в таблице entities
AFTER INSERT OR UPDATE
Заполнение даннымиvz_vds
id entity_id24786 138798 278969 3
serversid entity_id
24786 138798 278969 3
entitiesid123
usersid entity_id
24786 138798 278969 3INSERT
UPDATE SET entity_id
servicesid user_id
724786 78969338798978969
server_id
6783
vds_id
2786
entity_id3
26365
1.
2.
UPDATE SET entity_id
Обертка в ORM
Ent::User
Ent
EntHash
EntHash::User
Schema::Result::User
Schema::Result::Entity
Schema::Result::Lbill
hash_class()
tiehash
EntHash::ProxyAux
EntHash::Proxy
client_id
is_proxied()
is_proxied()
Schema::ResultSet::User
list() search(){ prefetch => { entity =>'lbill' } }
client_idis_proxied($_)
lbill.client_identity.$_
Неспешная миграция
entity.$field
users.$fields
servers.$fieldsvz_vds.$field
services.user_id
services.server_idservices.vds_id
services.entity_id
1.
2. Ent::XXX>new() Schema>resultset('XXX')>new()
3. SELECT * FROM xxx Schema>resultset('XXX')>search()
Завершение рефакторинга● Удаление переехавших в entities полей из
таблиц users, servers, vz_vds; полей user_id, server_id, vz_vds_id из таблицы services
● Удаление миграционных триггеров● Удаление оберточных классов и прочих
миграционных подпорок
Timeline рефакторинга
t
Создание таблиц
Написание триггеров Обертка ORMЗаполнение даннымиЗамена plain SQLмодификаций
Удаление ненужных полей и подпорок
Замена plain SQLзапросов
users.$field →entities.$field
1 релиз
1 релиз
N релизов
Вопросы?