Поэтапный 
рефакторинг: 
success story
Алексей Машанов
Цели рефакторинга
●   Упрощение добавления новых возможностей за счет
    возможности реиспользования кода.
●   Упрощение сопровождения кода за счет приведения его в
    человекочитаемый вид, нормализации кода и структуры базы.
●   Избавление от велосипедов и перенос тем самым головной
    боли по их развитию и поддержке на сообщество.
Характеристики системы
●   Perl + PostgreSQL
●   ~ 1200 модулей и 400 скриптов
●   ~ 300000 строк чистого кода
●   ~ 450 таблиц в БД
●   ~ 150 хранимых процедур и 140
    триггеров
Разбиение на этапы
                    Этап рефакторинга —      коммит
                    должен укладываться 
                    в рамки одного релиза

                                                              t

    Релиз (3­4 недели)              Шаг рефакторинга — 
                                    изменение не сказывающееся 
                                    на работоспособности системы

Рефакторинг выполняется в основной ветке разработки
Test Driven Refactioring
      Для каждого вносимого в код изменения


               Написание автотеста


Проверка автотеста путем поломки тестируемого кода


       Внесение модификации (рефакторинг)


   Проверка модифицированного кода автотестом
Структура автотестов
                                              Test::Class


                                               My::Test       rollback после 
lib/                      t/lib/
                                                              каждого теста

  Class1                       Class1::Test

  Class2   Class2::Sub1        Class2::Test                 Class2::Sub1::Test

  Class3   Class2::Sub2        Class3::Test                 Class2::Sub2::Test
I. Замена самописных ORM
       на DBIx::Class
Структура до рефакторинга
         Ent           Ent::Smth11               Entity     Entity::Smth11
new()                                   new()
                       Ent::Smth12                          Entity::Smth12
list()                                  list()
get()                     …             get()                    …
set()                                   set()
save()                Ent::Smth1N       save()              Entity::Smth1N

●   Два примерно одинаковых ORM
●   Методы модификации и поиска объединены в одном классе
●   Доступ к полям объекта как к элементам хэша
Что хотим получить
DBIx::Class::Row   Schema::Result::Smth11   DBIx::Class::ResultSet   Schema::ResultSet::Smth11
new()              Schema::Result::Smth12   search()                 Schema::ResultSet::Smth12
get_column()                …                                                   …
set_column()       Schema::Result::Smth1N                            Schema::ResultSet::Smth1N
insert()
update()
                   Schema::Result::Smth21                            Schema::ResultSet::Smth21
                   Schema::Result::Smth22                            Schema::ResultSet::Smth22
                            …                                                   …
                   Schema::Result::Smth2N                            Schema::ResultSet::Smth2N

●   Один ORM
●   Методы модификации и поиска в разных классах
●   Доступ к полям объекта через акцессоры
Зачем?
●   До рефакторинга
    ●   Два самописных ORM в одной системе это слишком много
    ●   Оба из них не поддерживают отношений между таблицами, тем не менее они нам
        необходимы, что приводит к обилию в коде plain SQL запросов
    ●   Вновьприбывшие разработчики вынуждены с ними разбираться и вникать в их отличия
●   После рефакторинга
    ●   Много новых хороших возможностей, которым мы все очень рады
    ●   Мы не одни во вселенной: почти все что нам может понадобиться уже изобрели,
        реализовали, отладили и устранили почти все баги, а какие не устранили, устраняют
        довольно-таки быстро
    ●   Опыт работы с DBIx::Class разработчику пригодится не только для работы над нашей
        системой, поэтому он с большей вероятностью потрудится разобраться в нем
        поглубже
Создание схемы
           table
          table
         table
                               схема
                    DBIx::Class::Schema::Loader

                         code style conventions
                           simple perl script

                         выстраиваем нужную 
                              иерархию              Schema::Result::*


Используем статическую схему                      DBIx::Class::Schema
Схема обертки
                tiehash
         Ent               EntHash     Schema::Result::SmthX
new()                     FETCH()     get_coumn()
set()                     STORE()     set_column()
get()                     EXISTS()    has_column_loaded()
list()                    NEXTKEY()   columns()
save()                    _DBIC_      insert_or_update()
                          _DBICRS_
  Ent::SmthN
 Ent::Smth2                           Schema::ResultSet::SmthX
 dbic_class()
Ent::Smth1
dbic_class()                          search()
dbic_class()
Callback методы
DBIx::Class::Core                                        Ent::*
                                                      save()
DBIC::EntCallback                                     delete()
insert()
update()
delete()
                       да                       нет
Schema::Result::*           caller() eq 'Ent'
Неспешная миграция
1.   Ent::XXX­>...        Schema­>resultset('XXX')­>...

2.   SELECT * FROM xxx    Schema­>resultset('XXX')­>select()


3.   Ent::XXX­>save()     Schema::Result::XXX­>insert()
                          Schema::Result::XXX­>update()

     Ent::XXX­>delete()   Schema::Result::XXX­>delete()
Завершение рефакторинга
●   Удаление иерархии старых ORM
Timeline рефакторинга
                  Схема таблиц Schema::*
        Обертка Ent вокруг DBIx::Class                         1 релиз
     Callback методов

                              Перенос хуков в Избавление
      Замена Ent::* →
                              Schema::Result::* от plain SQL   N релизов
    Schema­>resultset('*')


            Удаление старого ORM                               1 релиз
t
II. Единый механизм
хранения сущностей
Исходная структура
                                            Service связан с объектом одного из трех
       Lbill             Client
                                        ●


                                            классов, а не с одним.
                                        ●   User, Server, VDS имеют примерно
                                            одинаковый набор финансовых полей, но
                                            не используют наследование.
                                        ●   Лишняя связь от User, Server, VDS к Client.
User           Server             VDS
                                        ●   Сложные запросы к базе со
                                            множественными LEFT JOIN.
                                        ●   Добавление новой сущности приводит к
                                            созданию 1 класса, 3 связей и
                                            модификации Service.
               Service
Желаемая структура
                    ●   Добавление новой сущности
Client                  приводит к добавлению 1 класса и 1
                        связи.

 Lbill      User    ●   Финансовые операции ограничены
                        работой с Entity, а не с тремя User,
                        Server, VDS.
Entity     Server
                    ●   При добавлении новой сущности
                        большинство возможностей (кроме
Service     VDS         технических) - «из коробки».
                    ●   Нет лишних связей (нормализация).
Структура базы
        vz_vds                                                                  vz_vds
      servers                            clients                               servers
   id                                                                       id
      users                         id                                         users
 id                                                                       identity_id
   client_id
id                                                                       id
                                                                          entity_id
 client_id                                lbills                            технические 
   lbill_id                                                              entity_id
client_id                                                                 технические 
 lbill_id                           id                                      поля
lbill_id
   финансовые                                                            технические 
                                                                          поля
 финансовые                         client_id                            поля
   поля
финансовые 
 поля
   технические 
поля                     services                     services                entities
 технические 
   поля
технические 
 поля                user_id                       entity_id             id
поля                 server_id                                           lbill_id
                     vz_vds_id                                           финансовые 
                                                                         поля
                  Было                                           Стало
Миграционные триггеры
entities                           AFTER UPDATE
               Обновление соответствующих финансовых полей в 
               таблицах users, servers, vz_vds при их изменении

                                   BEFORE INSERT
 vz_vds
               1.Проверка, что все синхронизируемые из entities поля IS 
servers
               NULL — это означает, что не выполняется попытка 
users
               установить их значение при INSERT
               2.Автоматическое заполнение синхронизируемых полей 
               данными из соответствующей записи в entities

                             AFTER INSERT OR UPDATE
               Проверка, что все значения полей соответствуют 
               значениям всех соответствующих полей в таблице entities
Заполнение данными
                                                            vz_vds
                                                      id   servers entity_id
            entities                               24786
                                                    id                 1
                                                           users entity_id
                                                   38798               2
1.   id                                           24786
                                                   id
                                                   78969
                                                  38798
                                                                         1
                                                                    entity_id
                                                                          3
     1                                           24786                  12
     2                                            78969
                                                 38798                  23
     3
                                 INSERT          78969                  3
                           UPDATE SET entity_id


2.                                    services
                    id         user_id server_id vds_id      entity_id
                  724786       78969                             3
                  338798                          2786          26
                  978969                 6783                  365

          UPDATE SET entity_id
Обертка в ORM
                                  EntHash                           Schema::Result::User

                                                     is_proxied()
                              EntHash::Proxy                        Schema::Result::Entity

                                                     client_id
                           EntHash::ProxyAux                        Schema::Result::Lbill

               tiehash
         Ent                  EntHash::User
                           is_proxied()
   Ent::User
hash_class()                                                     Schema::ResultSet::User
                    { prefetch => { entity =>'lbill' } }
list()                                                           search()
                 client_id                 lbill.client_id
                 is_proxied($_)            entity.$_
Неспешная миграция
1.   users.$fields
     servers.$fields      entity.$field
     vz_vds.$field
     services.user_id
     services.server_id   services.entity_id
     services.vds_id

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 рефакторинга
                   Создание таблиц
Написание триггеров Заполнение данными    Обертка ORM     1 релиз
 Замена plain SQL
   модификаций
                     users.$field →    Замена plain SQL
                     entities.$field      запросов
                                                          N релизов


         Удаление ненужных полей и подпорок               1 релиз
t
Вопросы?

Aleksey Mashanov Rit

  • 1.
  • 2.
    Цели рефакторинга ● Упрощение добавления новых возможностей за счет возможности реиспользования кода. ● Упрощение сопровождения кода за счет приведения его в человекочитаемый вид, нормализации кода и структуры базы. ● Избавление от велосипедов и перенос тем самым головной боли по их развитию и поддержке на сообщество.
  • 3.
    Характеристики системы ● Perl + PostgreSQL ● ~ 1200 модулей и 400 скриптов ● ~ 300000 строк чистого кода ● ~ 450 таблиц в БД ● ~ 150 хранимых процедур и 140 триггеров
  • 4.
    Разбиение на этапы Этап рефакторинга —  коммит должен укладываться  в рамки одного релиза t Релиз (3­4 недели) Шаг рефакторинга —  изменение не сказывающееся  на работоспособности системы Рефакторинг выполняется в основной ветке разработки
  • 5.
    Test Driven Refactioring Для каждого вносимого в код изменения Написание автотеста Проверка автотеста путем поломки тестируемого кода Внесение модификации (рефакторинг) Проверка модифицированного кода автотестом
  • 6.
    Структура автотестов Test::Class My::Test rollback после  lib/ t/lib/ каждого теста Class1 Class1::Test Class2 Class2::Sub1 Class2::Test Class2::Sub1::Test Class3 Class2::Sub2 Class3::Test Class2::Sub2::Test
  • 7.
  • 8.
    Структура до рефакторинга Ent Ent::Smth11 Entity Entity::Smth11 new() new() Ent::Smth12 Entity::Smth12 list() list() get() … get() … set() set() save() Ent::Smth1N save() Entity::Smth1N ● Два примерно одинаковых ORM ● Методы модификации и поиска объединены в одном классе ● Доступ к полям объекта как к элементам хэша
  • 9.
    Что хотим получить DBIx::Class::Row Schema::Result::Smth11 DBIx::Class::ResultSet Schema::ResultSet::Smth11 new() Schema::Result::Smth12 search() Schema::ResultSet::Smth12 get_column() … … set_column() Schema::Result::Smth1N Schema::ResultSet::Smth1N insert() update() Schema::Result::Smth21 Schema::ResultSet::Smth21 Schema::Result::Smth22 Schema::ResultSet::Smth22 … … Schema::Result::Smth2N Schema::ResultSet::Smth2N ● Один ORM ● Методы модификации и поиска в разных классах ● Доступ к полям объекта через акцессоры
  • 10.
    Зачем? ● До рефакторинга ● Два самописных ORM в одной системе это слишком много ● Оба из них не поддерживают отношений между таблицами, тем не менее они нам необходимы, что приводит к обилию в коде plain SQL запросов ● Вновьприбывшие разработчики вынуждены с ними разбираться и вникать в их отличия ● После рефакторинга ● Много новых хороших возможностей, которым мы все очень рады ● Мы не одни во вселенной: почти все что нам может понадобиться уже изобрели, реализовали, отладили и устранили почти все баги, а какие не устранили, устраняют довольно-таки быстро ● Опыт работы с DBIx::Class разработчику пригодится не только для работы над нашей системой, поэтому он с большей вероятностью потрудится разобраться в нем поглубже
  • 11.
    Создание схемы table table table схема DBIx::Class::Schema::Loader code style conventions simple perl script выстраиваем нужную  иерархию Schema::Result::* Используем статическую схему DBIx::Class::Schema
  • 12.
    Схема обертки tiehash Ent EntHash Schema::Result::SmthX new() FETCH() get_coumn() set() STORE() set_column() get() EXISTS() has_column_loaded() list() NEXTKEY() columns() save() _DBIC_ insert_or_update() _DBICRS_ Ent::SmthN Ent::Smth2 Schema::ResultSet::SmthX dbic_class() Ent::Smth1 dbic_class() search() dbic_class()
  • 13.
    Callback методы DBIx::Class::Core Ent::* save() DBIC::EntCallback delete() insert() update() delete() да нет Schema::Result::* caller() eq 'Ent'
  • 14.
    Неспешная миграция 1. Ent::XXX­>... Schema­>resultset('XXX')­>... 2. SELECT * FROM xxx Schema­>resultset('XXX')­>select() 3. Ent::XXX­>save() Schema::Result::XXX­>insert() Schema::Result::XXX­>update() Ent::XXX­>delete() Schema::Result::XXX­>delete()
  • 15.
    Завершение рефакторинга ● Удаление иерархии старых ORM
  • 16.
    Timeline рефакторинга Схема таблиц Schema::* Обертка Ent вокруг DBIx::Class 1 релиз Callback методов Перенос хуков в Избавление Замена Ent::* → Schema::Result::* от plain SQL N релизов Schema­>resultset('*') Удаление старого ORM 1 релиз t
  • 17.
  • 18.
    Исходная структура Service связан с объектом одного из трех Lbill Client ● классов, а не с одним. ● User, Server, VDS имеют примерно одинаковый набор финансовых полей, но не используют наследование. ● Лишняя связь от User, Server, VDS к Client. User Server VDS ● Сложные запросы к базе со множественными LEFT JOIN. ● Добавление новой сущности приводит к созданию 1 класса, 3 связей и модификации Service. Service
  • 19.
    Желаемая структура ● Добавление новой сущности Client приводит к добавлению 1 класса и 1 связи. Lbill User ● Финансовые операции ограничены работой с Entity, а не с тремя User, Server, VDS. Entity Server ● При добавлении новой сущности большинство возможностей (кроме Service VDS технических) - «из коробки». ● Нет лишних связей (нормализация).
  • 20.
    Структура базы vz_vds vz_vds servers clients servers id id users id users id identity_id client_id id id entity_id client_id lbills технические  lbill_id entity_id client_id технические  lbill_id id поля lbill_id финансовые  технические  поля финансовые  client_id поля поля финансовые  поля технические  поля services services entities технические  поля технические  поля user_id entity_id id поля server_id lbill_id vz_vds_id финансовые  поля Было Стало
  • 21.
    Миграционные триггеры entities AFTER UPDATE Обновление соответствующих финансовых полей в  таблицах users, servers, vz_vds при их изменении BEFORE INSERT vz_vds 1.Проверка, что все синхронизируемые из entities поля IS  servers NULL — это означает, что не выполняется попытка  users установить их значение при INSERT 2.Автоматическое заполнение синхронизируемых полей  данными из соответствующей записи в entities AFTER INSERT OR UPDATE Проверка, что все значения полей соответствуют  значениям всех соответствующих полей в таблице entities
  • 22.
    Заполнение данными vz_vds id servers entity_id entities 24786 id 1 users entity_id 38798 2 1. id 24786 id 78969 38798 1 entity_id 3 1 24786 12 2 78969 38798 23 3 INSERT 78969 3 UPDATE SET entity_id 2. services id user_id server_id vds_id entity_id 724786 78969 3 338798 2786 26 978969 6783 365 UPDATE SET entity_id
  • 23.
    Обертка в ORM EntHash Schema::Result::User is_proxied() EntHash::Proxy Schema::Result::Entity client_id EntHash::ProxyAux Schema::Result::Lbill tiehash Ent EntHash::User is_proxied() Ent::User hash_class() Schema::ResultSet::User { prefetch => { entity =>'lbill' } } list() search() client_id lbill.client_id is_proxied($_) entity.$_
  • 24.
    Неспешная миграция 1. users.$fields servers.$fields entity.$field vz_vds.$field services.user_id services.server_id services.entity_id services.vds_id 2. Ent::XXX­>new() Schema­>resultset('XXX')­>new() 3. SELECT * FROM xxx Schema­>resultset('XXX')­>search()
  • 25.
    Завершение рефакторинга ● Удаление переехавших в entities полей из таблиц users, servers, vz_vds; полей user_id, server_id, vz_vds_id из таблицы services ● Удаление миграционных триггеров ● Удаление оберточных классов и прочих миграционных подпорок
  • 26.
    Timeline рефакторинга Создание таблиц Написание триггеров Заполнение данными Обертка ORM 1 релиз Замена plain SQL модификаций users.$field → Замена plain SQL entities.$field запросов N релизов Удаление ненужных полей и подпорок 1 релиз t
  • 27.