Vm — це текстовий редактор, подібний до Vim, який я створив з нуля на C++ разом із Mohammed Nafees. Ми мали дуже обмежений час — всього 3 тижні, з яких 1 тиждень пішов на дизайн (UML-діаграми, о Боже…) і планування. Останні два тижні були сповнені веселощів — програмуванням і стресом!
У цій статті, одній із багатьох, ми поговоримо про загальний дизайн нашої програми, а також про концепції об'єктно-орієнтованого програмування (OOP) (Object-Oriented Programming) і шаблони проектування, які ми вивчили під час розробки цього проекту.
Коротка історія про Vim
Vi — це оригінальний екранно-орієнтований текстовий редактор, створений для операційної системи Unix, який написав Білл Джой у 1976 році.
Vim (Vi IMproved) — це значно покращений клон Vi, розроблений Брамом Муленаром, який був випущений в 1991 році. З того часу він став одним із найбільш популярних текстових редакторів у світі, увійшовши до топ-4 найбільш популярних середовищ розробки за даними опитування Stack Overflow серед розробників у 2017 році.
Приклад використання Vim
Наступна стаття поділена на:
- Дизайн програмного забезпечення
- Шаблони проектування
- Структури даних для тексту
Те, що ми дізналися, створюючи Vm
Ось ваш перший погляд на Vm! Хіба він не гарний?
Програма "Hello World" у Vm!
Принципи дизайну
SOLID
Термін SOLID є мнемонічним акронімом для п’яти принципів проектування, які спрямовані на те, щоб зробити дизайни програмного забезпечення більш зрозумілими, гнучкими та підтримуваними.
Чудовий дизайн веде до чудового досвіду
Принцип єдиної відповідальності (Single Responsibility): Кожен з наших класів має одну єдину відповідальність — виконувати одну задачу, окрім класу Controller
. Якщо ви не можете легко сказати, що клас виконує x, подумайте про те, щоб розділити його на менші частини.
Принцип відкритості/закритості (Open/Closed Principle): Програмне забезпечення має бути відкритим для розширення, але закритим для модифікації. Якщо зміна в специфікації проекту вимагає зміни інтерфейсу класу, або ж це лише означає, що потрібно розширити існуючий клас?
Клас View
відкритий для розширення, але закритий для модифікації. Ми хотіли створити додатковий вигляд, що відображатиме певні значення, позиції курсора та стек команд, тому замість того, щоб додавати цю функціональність до View
, ми створили підклас DebugView
, який містить необхідну функціональність. Всі члени змінних класів є приватними для кожного класу, і ми маємо геттерів та сеттерів лише коли це необхідно.
Принцип заміщення Ліскова (Liskov Substitution): Чи залишається ваш код правильним, якщо ви заміните об'єкт типу A на об'єкт типу B, якщо B — підклас A? Чи не ламається програма, якщо ви підставите більш загальний (абстрактний) клас? Не повинно. І ви можете використовувати цей факт на свою користь, щоб написати чудовий код.
Ми використовували принцип Ліскова переважно в двох частинах: команди та вигляди. Для будь-якого підтипу команди, заміна одного на інший не спричинить збою програми або непередбачувану поведінку (окрім того, що команда виконує іншу серію дій). Можна з впевненістю стверджувати, що виклик canUndo
, execute
та undo
на будь-якому підтипі Command
працюватиме.
Команди, які показуються: 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), який насправді виглядає досить інтуїтивно.
Шаблон проектування
Ми використали Шаблон Команди (Command Pattern) для обробки функції відміни. Команди Vm були інкапсульовані в багато класів команд (конкретні підкласи), які містять 2 основні методи (execute
і undo
). Потім ці команди зберігалися в стеку Command
(абстрактний суперклас). Коли виконується команда undo, вона викликає метод undo
верхнього елемента стеку команд, і потім цей елемент вилучається зі стеку. Виклик методу execute
фактично виконує команду. Недолік цього шаблону в тому, що потрібно створювати багато підкласів для кожної окремої функції, що не завжди здається хорошим об'єктно-орієнтованим підходом. Але це зробить вашу функцію відміни такою легкою, як
Структури даних для тексту
Ми витратили багато часу, обираючи, яку структуру даних використовувати для зберігання та маніпуляції текстом у файлах. Які з них будуть найшвидшими, найбільш ефективними за пам'яттю та найбільш зрозумілими для кодування? Все це було дуже цікаво обговорювати. На жаль, у нас був ще один обмежувальний фактор — термін у 3 тижні. Тому ми вирішили використовувати вектор рядків.
Це наш клас 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