Проектування та створення Vim з нуля за 3 тижні

Vm — це текстовий редактор, подібний до Vim, який я створив з нуля на C++ разом із Mohammed Nafees. Ми мали дуже обмежений час — всього 3 тижні, з яких 1 тиждень пішов на дизайн (UML-діаграми, о Боже…) і планування. Останні два тижні були сповнені веселощів — програмуванням і стресом!

У цій статті, одній із багатьох, ми поговоримо про загальний дизайн нашої програми, а також про концепції об'єктно-орієнтованого програмування (OOP) (Object-Oriented Programming) і шаблони проектування, які ми вивчили під час розробки цього проекту.

pic

Коротка історія про Vim

Vi — це оригінальний екранно-орієнтований текстовий редактор, створений для операційної системи Unix, який написав Білл Джой у 1976 році.

Vim (Vi IMproved) — це значно покращений клон Vi, розроблений Брамом Муленаром, який був випущений в 1991 році. З того часу він став одним із найбільш популярних текстових редакторів у світі, увійшовши до топ-4 найбільш популярних середовищ розробки за даними опитування Stack Overflow серед розробників у 2017 році.

pic

Приклад використання Vim

Наступна стаття поділена на:

  1. Дизайн програмного забезпечення
  2. Шаблони проектування
  3. Структури даних для тексту

Те, що ми дізналися, створюючи Vm

Ось ваш перший погляд на Vm! Хіба він не гарний?

pic

Програма "Hello World" у Vm!

Принципи дизайну

SOLID

Термін SOLID є мнемонічним акронімом для п’яти принципів проектування, які спрямовані на те, щоб зробити дизайни програмного забезпечення більш зрозумілими, гнучкими та підтримуваними.

-Wikipedia

pic

Чудовий дизайн веде до чудового досвіду

Принцип єдиної відповідальності (Single Responsibility): Кожен з наших класів має одну єдину відповідальність — виконувати одну задачу, окрім класу Controller. Якщо ви не можете легко сказати, що клас виконує x, подумайте про те, щоб розділити його на менші частини.

Принцип відкритості/закритості (Open/Closed Principle): Програмне забезпечення має бути відкритим для розширення, але закритим для модифікації. Якщо зміна в специфікації проекту вимагає зміни інтерфейсу класу, або ж це лише означає, що потрібно розширити існуючий клас?

Клас View відкритий для розширення, але закритий для модифікації. Ми хотіли створити додатковий вигляд, що відображатиме певні значення, позиції курсора та стек команд, тому замість того, щоб додавати цю функціональність до View, ми створили підклас DebugView, який містить необхідну функціональність. Всі члени змінних класів є приватними для кожного класу, і ми маємо геттерів та сеттерів лише коли це необхідно.

Принцип заміщення Ліскова (Liskov Substitution): Чи залишається ваш код правильним, якщо ви заміните об'єкт типу A на об'єкт типу B, якщо B — підклас A? Чи не ламається програма, якщо ви підставите більш загальний (абстрактний) клас? Не повинно. І ви можете використовувати цей факт на свою користь, щоб написати чудовий код.

Ми використовували принцип Ліскова переважно в двох частинах: команди та вигляди. Для будь-якого підтипу команди, заміна одного на інший не спричинить збою програми або непередбачувану поведінку (окрім того, що команда виконує іншу серію дій). Можна з впевненістю стверджувати, що виклик canUndo, execute та undo на будь-якому підтипі Command працюватиме.

pic

Команди, які показуються: A, (5) i, I

Принцип сегрегації інтерфейсів (Interface Segregation): Краще мати багато маленьких інтерфейсів, ніж один великий.
Якщо клас має багато функціональностей, кожен клієнт класу повинен бачити лише ту функціональність, яку він потребує.

Більшість інтерфейсів у нашому проекті потрібно було створювати лише для одного клієнта, тому не було потреби робити більш специфічний інтерфейс для кожного клієнта. Але це гарне загальне правило!

Інверсія залежностей (Dependency Inversion): Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва повинні залежати від абстракцій. Абстрактні класи ніколи не повинні залежати від конкретних класів.

Всі повністю абстрактні класи, які ми мали (Command.h), не успадковувалися від конкретних класів і не залежали від них. Усі класи взаємодіяли та залежали від інших класів того ж рівня абстракції. Наприклад, клас View залежить/взаємодіє з іншими класами високого рівня, такими як Highlighter і Cursor, а не з їх підкласами низького рівня або підлягаючими їм структурами даних, як-от CppHighlighter чи ViewCursor.

Модель-Погляд-Контролер

У рамках принципу єдиної відповідальності ми створили три класи, кожен з яких обробляє одну частину Vm з точки зору додатку, що взаємодіє з користувачем.

Модель: Application.h Application.cc

Клас Application відповідає за збереження стану поточної інстанції через MODE і вказує контролеру діяти відповідно.

Погляд: View.h View.cc

Клас View відповідає за виведення вмісту буфера або будь-якого іншого виведення на екран та синтаксичне підсвічування (якщо це файл C++).

Контролер: Controller.h Controller.cc

Клас Controller обробляє кожен ввід залежно від поточного MODE редактора та вносить зміни в View через класи ViewCursor та/або Buffer.

Шаблони проектування

Загальні принципи SOLID справді виправдали себе, адже ми не потрапляли в багато ситуацій, де не знали б, який шлях дизайну обрати. Ми не використали багато шаблонів проектування, як ми й думали, і я не вважаю, що це погано. Ви ніколи не повинні намагатися вставити охайний шаблон проектування просто для того, щоб його використати. Один шаблон, який насправді нас врятував, — це шаблон Команди (Command pattern), який насправді виглядає досить інтуїтивно.

pic

Шаблон проектування

Ми використали Шаблон Команди (Command Pattern) для обробки функції відміни. Команди Vm були інкапсульовані в багато класів команд (конкретні підкласи), які містять 2 основні методи (execute і undo). Потім ці команди зберігалися в стеку Command (абстрактний суперклас). Коли виконується команда undo, вона викликає метод undo верхнього елемента стеку команд, і потім цей елемент вилучається зі стеку. Виклик методу execute фактично виконує команду. Недолік цього шаблону в тому, що потрібно створювати багато підкласів для кожної окремої функції, що не завжди здається хорошим об'єктно-орієнтованим підходом. Але це зробить вашу функцію відміни такою легкою, як

Структури даних для тексту

Ми витратили багато часу, обираючи, яку структуру даних використовувати для зберігання та маніпуляції текстом у файлах. Які з них будуть найшвидшими, найбільш ефективними за пам'яттю та найбільш зрозумілими для кодування? Все це було дуже цікаво обговорювати. На жаль, у нас був ще один обмежувальний фактор — термін у 3 тижні. Тому ми вирішили використовувати вектор рядків.

pic

Це наш клас Buffer, який працює з текстом файлу.

Буфер з проміжком

Якби ми могли повернутися і переписати наш Buffer, ймовірно, ми б використали Gap Buffer. Gap Buffer використовує факт, що зміни в тексті локалізовані, зазвичай поруч з курсором.
Текст зберігається в двох великих сегментах з великим проміжком між ними для вставлення нового тексту.

Недолік Gap Buffer полягає в тому, що коли проміжок заповнюється або змінюється позиція курсора, текст потрібно перерасподіляти на два нові сегменти, що є дорогим процесом копіювання тексту, особливо в великих файлах. Однак такі випадки набагато рідші, і припускається, що витрати компенсуються швидкими звичайними операціями. Emacs використовує Gap Buffer як основну структуру даних для тексту!

Таблиця фрагментів (Piece Table)

Таблиця фрагментів — це промислова структура даних для тексту. Вона використовувалась в Microsoft Word, Atom, Visual Studio Code та багатьох інших високоякісних текстових редакторах. Але це значно виходить за межі цього проекту.

Створення власного текстового редактора навчить вас багато чого про написання якісного програмного забезпечення, проте я вважаю, що це неефективне використання часу та енергії, якщо кожен просто повторює цю вправу заради її. Якщо вам нудно і ви хочете похизуватись своїми навичками кодування, чому б не внести свій внесок в один із цих чудових відкритих текстових редакторів:

Використані сторонні програми та бібліотеки:

  • NCurses
  • Boost
  • CMake

Це завершення частини 1 серії постів про наш проект Vm. Було дуже весело писати це, і я сподіваюся написати більше в майбутньому!

Ми ще не закінчили…

У процесі написання разом з Mohammed Nafees:

  • Синтаксичне підсвічування та пошук у Vm
  • Як написати програмне забезпечення, яке стійке до змін
  • Як ми ефективно (і не дуже) використовували Git

Завантажити для Linux систем: http://s000.tinyupload.com/index.php?file_id=14499969885562543639

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

Перекладено з: Designing And Writing Vim From Scratch In 3 Weeks

Leave a Reply

Your email address will not be published. Required fields are marked *