Разработването на софтуер е чудесно, но ... Мисля, че всички можем да се съгласим, че може да е малко емоционално влакче. В началото всичко е страхотно. Добавяте нови функции една след друга за броени дни, ако не и часове. Вие сте на път!
Бързо напред няколко месеца и скоростта на вашето развитие намалява. Дали защото не работите толкова усилено, колкото преди? Не точно. Нека ускорим още няколко месеца напред и скоростта ви на развитие ще спадне още повече. Работата по този проект вече не е забавна и се превърна в плъзгане.
Влошава се. Започвате да откривате множество грешки във вашето приложение. Често решаването на една грешка създава две нови. На този етап можете да започнете да пеете:
99 малки грешки в кода. 99 малки бъгове. Свалете един, закърпете го,
... 127 малки грешки в кода.
Какво чувствате за работата по този проект сега? Ако сте като мен, вероятно започвате да губите мотивацията си. Това е просто мъка за разработването на това приложение, тъй като всяка промяна на съществуващия код може да има непредсказуеми последици.
Този опит е често срещан в света на софтуера и може да обясни защо толкова много програмисти искат да изхвърлят изходния си код и да пренапишат всичко.
И така, каква е причината за този проблем?
Основната причина е нарастващата сложност. От моя опит най-голям принос за цялостната сложност има фактът, че в по-голямата част от софтуерните проекти всичко е свързано. Поради зависимостите, които има всеки клас, ако промените някакъв код в класа, който изпраща имейли, вашите потребители изведнъж не могат да се регистрират. Защо така? Тъй като регистрационният ви код зависи от кода, който изпраща имейли. Сега не можете да промените нищо, без да въвеждате грешки. Просто не е възможно да се проследят всички зависимости.
И така, имате го; истинската причина за нашите проблеми е повишаването на сложността, идваща от всички зависимости, които нашият код има.
Смешното е, че този въпрос е известен от години. Това е често срещан анти-модел, наречен „голямата топка кал“. Виждал съм такъв тип архитектура в почти всички проекти, по които съм работил през годините в множество различни компании.
И така, какъв точно е този анти-модел? Просто казано, получавате голяма топка кал, когато всеки елемент има зависимост с други елементи. По-долу можете да видите графика на зависимостите от добре познатия проект с отворен код Apache Hadoop. За да визуализирате голямата топка кал (или по-скоро голямата топка прежда), вие нарисувате кръг и равномерно поставяте класове от проекта върху него. Просто начертайте линия между всяка двойка класове, които зависят един от друг. Сега можете да видите източника на проблемите си.
Затова си зададох въпрос: Би ли било възможно да се намали сложността и все пак да се забавлявате като в началото на проекта? Честно казано, не можете да елиминирате всичко на сложността. Ако искате да добавите нови функции, винаги ще трябва да повишавате сложността на кода. Въпреки това сложността може да бъде преместена и разделена.
Помислете за механичната индустрия. Когато някой малък механичен магазин създава машини, те купуват набор от стандартни елементи, създават няколко персонализирани и ги сглобяват. Те могат да направят тези компоненти напълно отделно и да сглобят всичко в края, като правят само няколко ощипвания. Как е възможно? Те знаят как всеки елемент ще се побере заедно чрез определени индустриални стандарти като размери на болтовете и решения отпред като размера на монтажните отвори и разстоянието между тях.
Всеки елемент в сглобката по-горе може да бъде предоставен от отделна компания, която няма никакви познания за крайния продукт или другите му части. Докато всеки модулен елемент е произведен в съответствие със спецификациите, вие ще можете да създадете крайното устройство, както е планирано.
Можем ли да повторим това в софтуерната индустрия?
Разбира се, че можем! Чрез използване на интерфейси и инверсия на принципа на управление; най-добрата част е фактът, че този подход може да се използва във всеки обектно-ориентиран език: Java, C #, Swift, TypeScript, JavaScript, PHP - списъкът продължава и продължава. За да приложите този метод, не ви е необходима изискана рамка. Просто трябва да се придържате към няколко прости правила и да останете дисциплинирани.
Когато за първи път чух за инверсия на контрола, веднага разбрах, че съм намерил решение. Това е концепция за вземане на съществуващи зависимости и тяхното обръщане чрез използване на интерфейси. Интерфейсите са прости декларации на методи. Те не предоставят никакво конкретно изпълнение. В резултат на това те могат да се използват като споразумение между два елемента за това как да ги свържете. Те могат да се използват като модулни съединители, ако желаете. Докато един елемент осигурява интерфейса, а друг елемент осигурява изпълнението за него, те могат да работят заедно, без да знаят нищо един за друг. Брилянтно е.
Нека да видим на прост пример как можем да отделим нашата система, за да създадем модулен код. Диаграмите по-долу са внедрени като прости Java приложения. Можете да ги намерите на това Хранилище на GitHub .
Да приемем, че имаме много просто приложение, състоящо се само от Main
клас, три услуги и един Util
клас. Тези елементи зависят един от друг по множество начини. По-долу можете да видите изпълнение, използващо подхода „голяма топка кал“. Класовете просто се обаждат един на друг. Те са здраво свързани и не можете просто да извадите един елемент, без да докосвате други. Приложенията, създадени с помощта на този стил, ви позволяват първоначално да растете бързо. Вярвам, че този стил е подходящ за проекти с доказателство за концепция, тъй като можете лесно да си играете с нещата. Въпреки това не е подходящо за готови за производство решения, защото дори поддръжката може да бъде опасна и всяка една промяна може да създаде непредсказуеми грешки. Диаграмата по-долу показва тази голяма топка архитектура от кал.
В търсене на по-добър подход можем да използваме техника, наречена инжекция на зависимост. Този метод предполага, че всички компоненти трябва да се използват чрез интерфейси. Чел съм твърдения, че отделя елементи, но наистина ли е така? Не. Погледнете диаграмата по-долу.
Единствената разлика между текущата ситуация и голяма топка кал е фактът, че сега, вместо да извикваме класове директно, ние ги извикваме през техните интерфейси. Той леко подобрява разделящите елементи един от друг. Ако например искате да използвате повторно Service A
в различен проект можете да го направите, като извадите Service A
себе си, заедно с Interface A
, както и Interface B
и Interface Util
. Както можете да видите, Service A
все още зависи от други елементи. В резултат на това все още имаме проблеми със смяната на кода на едно място и объркване на поведението на друго. Все още създава проблема, че ако промените Service B
и Interface B
, ще трябва да промените всички елементи, които зависят от него. Този подход не решава нищо; по мое мнение, той просто добавя слой интерфейс върху елементите. Никога не трябва да инжектирате никакви зависимости, а вместо това трябва да се отървете от тях веднъж завинаги. Ура за независимост!
Подходът, който според мен решава всички основни главоболия на зависимостите, го прави, като изобщо не използва зависимости. Вие създавате компонент и неговия слушател. Слушателят е прост интерфейс. Винаги, когато трябва да извикате метод извън текущия елемент, просто добавяте метод към слушателя и вместо това го извиквате. Елементът има право да използва само файлове, методи за извикване в пакета си и да използва класове, предоставени от основната рамка или други използвани библиотеки. По-долу можете да видите диаграма на приложението, модифицирана да използва архитектура на елементи.
Моля, имайте предвид, че в тази архитектура само Main
клас има множество зависимости. Той свързва всички елементи заедно и капсулира бизнес логиката на приложението.
Услугите, от друга страна, са напълно независими елементи. Сега можете да извадите всяка услуга от това приложение и да ги използвате отново някъде другаде. Те не зависят от нищо друго. Но изчакайте, става по-добре: не е необходимо да променяте тези услуги никога повече, стига да не променяте поведението им. Докато тези услуги правят това, което трябва да направят, те могат да останат недокоснати до края на времето. Те могат да бъдат създадени от професионалист софтуерен инженер , или за първи път кодер, компрометиран от най-лошия код за спагети, който някога е готвил goto
смесени сметки. Няма значение, защото тяхната логика е капсулирана. Колкото и ужасно да е, никога няма да се разлее в други класове. Това също така ви дава силата да разделите работата в проект между множество разработчици, където всеки разработчик може да работи върху собствения си компонент независимо, без да е необходимо да прекъсва друг или дори да знае за съществуването на други разработчици.
И накрая, можете да започнете да пишете независим код още веднъж, точно както в началото на последния си проект.
Нека дефинираме структурата на структурния елемент, така че да можем да го създадем по повторяем начин.
Най-простата версия на елемента се състои от две неща: Основен клас елемент и слушател. Ако искате да използвате елемент, тогава трябва да внедрите слушателя и да осъществите повиквания към основния клас. Ето диаграма на най-простата конфигурация:
Очевидно в крайна сметка ще трябва да добавите повече сложност към елемента, но можете да го направите лесно. Просто се уверете, че нито един от вашите логически класове не зависи от други файлове в проекта. Те могат да използват само основната рамка, импортираните библиотеки и други файлове в този елемент. Когато става въпрос за файлове с активи като изображения, изгледи, звуци и т.н., те също трябва да бъдат капсулирани в елементи, така че в бъдеще да бъдат лесни за повторна употреба. Можете просто да копирате цялата папка в друг проект и ето го!
По-долу можете да видите примерна графика, показваща по-усъвършенстван елемент. Забележете, че се състои от изглед, който използва, и не зависи от други файлове на приложението. Ако искате да знаете прост метод за проверка на зависимостите, просто погледнете раздела за импортиране. Има ли файлове извън текущия елемент? Ако е така, тогава трябва да премахнете тези зависимости, като ги преместите в елемента или като добавите подходящо повикване към слушателя.
Нека да разгледаме и прост пример „Hello World“, създаден в Java.
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = 'Hello World of Elements!'; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }
Първоначално дефинираме ElementListener
за да зададете метода, който отпечатва изхода. Самият елемент е дефиниран по-долу. При повикване sayHello
върху елемента, той просто отпечатва съобщение, използвайки ElementListener
. Забележете, че елементът е напълно независим от изпълнението на printOutput
метод. Може да се отпечата в конзолата, физически принтер или изискан потребителски интерфейс. Елементът не зависи от това изпълнение. Поради тази абстракция този елемент може лесно да се използва в различни приложения.
Сега погледнете основното App
клас. Той изпълнява слушателя и сглобява елемента заедно с конкретна реализация. Сега можем да започнем да го използваме.
Можете също да стартирате този пример в JavaScript тук
Нека да разгледаме използването на модела на елемента в мащабни приложения. Едно е да го покажете в малък проект, а друго е да го приложите в реалния свят.
Структурата на пълно стек уеб приложение, което обичам да използвам, изглежда по следния начин:
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
В папка с изходен код първоначално разделяме клиентските и сървърните файлове. Това е разумно нещо да се направи, тъй като те работят в две различни среди: браузърът и сървърът отзад.
След това разделяме кода във всеки слой на папки, наречени app и елементи. Елементите се състоят от папки с независими компоненти, докато папката с приложения свързва всички елементи заедно и съхранява цялата бизнес логика.
По този начин елементите могат да се използват повторно между различните проекти, докато цялата специфична за приложението сложност се капсулира в една папка и доста често се свежда до прости извиквания на елементи.
Вярвайки, че практиката винаги надделява над теорията, нека разгледаме пример от реалния живот, създаден в Node.js и TypeScript.
Това е много просто уеб приложение, което може да се използва като отправна точка за по-модерни решения. Той следва архитектурата на елементите, както и използва широко структурен модел на елемент.
От акцентите можете да видите, че главната страница е отличена като елемент. Тази страница включва свой собствен изглед. Така че, когато например искате да го използвате повторно, можете просто да копирате цялата папка и да я пуснете в различен проект. Просто свържете всичко заедно и сте готови.
Това е основен пример, който показва, че можете да започнете да въвеждате елементи в собственото си приложение още днес. Можете да започнете да разграничавате независими компоненти и да отделяте тяхната логика. Няма значение колко объркан е кодът, по който работите в момента.
Надявам се, че с този нов набор от инструменти ще можете по-лесно да разработите код, който е по-поддържаем. Преди да преминете към използването на шаблона на елемент на практика, нека да обобщим бързо всички основни моменти:
Много проблеми в софтуера се случват поради зависимости между множество компоненти.
Като направите промяна на едно място, можете да въведете непредсказуемо поведение някъде другаде.
Три често срещани архитектурни подхода са:
Голямата топка кал. Това е чудесно за бързо развитие, но не е толкова добро за стабилни производствени цели.
Инжектиране на зависимост. Това е полупечено решение, което трябва да избягвате.
Архитектура на елементите. Това решение ви позволява да създавате независими компоненти и да ги използвате повторно в други проекти. Поддържа се и е брилянтен за стабилни производствени версии.
Основният модел елемент се състои от основен клас, който има всички основни методи, както и слушател, който е прост интерфейс, който позволява комуникация с външния свят.
За да постигнете архитектура на пълен стек, първо отделете вашия преден край от задния код. След това създавате папка във всяка за приложение и елементи. Папката с елементи се състои от всички независими елементи, докато папката с приложения свързва всичко заедно.
Сега можете да започнете да създавате и споделяте свои собствени елементи. В дългосрочен план това ще ви помогне да създадете лесно поддържаеми продукти. Успех и ми кажете какво сте създали!
Също така, ако откриете, че преждевременно оптимизирате кода си, прочетете Как да избегнем проклятието на преждевременната оптимизация от колегата ApeeScapeer Кевин Блок.
Свързани: JS Best Practices: Създайте Discord Bot с TypeScript и Dependency InjectionКодът може да бъде труден за поддръжка поради зависимости между множество компоненти. В резултат на това извършването на промени на едно място може да внесе непредсказуемо поведение някъде другаде.
Модулната архитектура означава разделяне на приложението на независими елементи. Ние признаваме всички междупроектни зависимости като причина за трудно намиране и отстраняване на проблеми. Пълната независимост прави тези компоненти изключително лесни за тестване, поддръжка, споделяне и повторна употреба в бъдеще.
Общите архитектурни подходи са: 1) Голямата топка кал: Страхотна за бързо развитие, но не толкова подходяща за стабилни производствени цели. 2) Инжектиране на зависимост: Полупечено решение, което трябва да избягвате. 3) Архитектура на елементите: Поддържа се и е блестяща за стабилни производствени версии.