Четири правила за по-опростен дизайн на iOS

В края на 90-те години на миналия век, докато разработваше Extreme Programming, известният софтуерен разработчик Kent Beck излезе със списък с правила за просто проектиране на софтуер.

Според Кент Бек, добър дизайн на софтуера:

  • Изпълнява всички тестове
  • Не съдържа дублиране
  • Изразява намерението на програмиста
  • Минимизира броя на класовете и методите

В тази статия ще обсъдим как тези правила могат да бъдат приложени към света за разработка на iOS, като дадем практически примери за iOS и ще обсъдим как можем да се възползваме от тях.

Изпълнява всички тестове

Софтуерният дизайн ни помага да създадем система, която действа по предназначение. Но как можем да потвърдим, че една система ще действа, както първоначално е предвидено от своя дизайн? Отговорът е чрез създаване на тестове, които го валидират.

За съжаление, във вселената тестовете за разработка на iOS са избягвани ... Но за да създадем добре проектиран софтуер, винаги трябва да пишем Swift код с доказателство.

Нека обсъдим два принципа, които могат да направят тестовото писане и дизайна на системата. И те са единичен принцип на отговорност и инжектиране на зависимост.

Принцип на единната отговорност (SRP)

SRP посочва, че клас трябва да има една и само една причина за промяна. SRP е един от най-простите принципи и един от най-трудните да се оправи. Смесването на отговорностите е нещо, което правим естествено.

Да дадем пример за някакъв код, който наистина е трудно да се тества и след това го рефакторирате, като използвате SRP. След това обсъдете как направи кодът тест.

Да предположим, че в момента трябва да представим PaymentViewController от нашия текущ контролер на изглед, PaymentViewController трябва да конфигурира изгледа си в зависимост от цената на нашия платежен продукт. В нашия случай цената е променлива в зависимост от някои външни потребителски събития.

Кодът за тази реализация в момента изглежда следният:

Как можем да тестваме този код? Какво трябва да тестваме първо? Правилно ли се изчислява отстъпката от цената? Как можем да се подиграем със събития за плащане, за да тестваме отстъпката?

Писането на тестове за този клас би било сложно, трябва да намерим по-добър начин да го напишем. Е, първо да се обърнем към големия проблем. Трябва да разплетим зависимостите си.

Виждаме, че имаме логика за зареждане на нашия продукт. Имаме събития за плащане, които правят потребителя допустим за отстъпка. Имаме отстъпки, изчисление на отстъпките и списъкът продължава.

Затова нека се опитаме просто да ги преведем в Swift код.

Създадохме PaymentManager, който управлява нашата логика, свързана с плащанията, и отделен PriceCalculator, който е лесно тестваем. Също така, заредител на данни, който отговаря за взаимодействието между мрежата или базата данни за зареждане на нашите продукти.

Споменахме също, че се нуждаем от клас, отговорен за управлението на отстъпките. Нека го наречем CouponManager и нека така добре управлява купоните за отстъпки на потребителите.

Нашият контролер за изглед на плащания след това може да изглежда по следния начин:

Вече можем да пишем тестове като

  • testCalculatingFinalPriceWithoutCoupon
  • testCalculatingFinalPriceWithCoupon
  • testCouponExists

и много други! Създавайки отделни обекти, ние избягваме ненужно дублиране и създадохме код, за който е лесно да пишете тестове.

Инжектиране на зависимостта

Вторият принцип е инжектирането на зависимост. И от примерите по-горе видяхме, че вече използвахме инжектиране на зависимост от нашите обектни инициализатори.

Има две основни ползи от инжектирането на нашите зависимости като по-горе. Изяснява се на какви зависимости разчитат нашите типове и ни позволява да вмъкваме макетни обекти, когато искаме да тестваме вместо реалните.

Добра техника е да създадем протоколи за нашите обекти и да осигурим конкретна реализация от реалния и макетния обект като следното:

Сега можем лесно да решим кой клас искаме да вложим като зависимост.

Стегнатото свързване затруднява писането на тестове. Така че, подобно, колкото повече тестове пишем, толкова повече използваме принципи като DIP и инструменти като инжектиране на зависимост, интерфейси и абстракция, за да сведем до минимум свързването.

Подобряването на кода не само премахва страха ни да го нарушим (тъй като ще напишем теста, който ще ни подкрепи), но и допринася за писането на по-чист код.

Тази част от статията беше загрижена повече за това как да напишете код, който ще бъде тест, отколкото да напишете реалния тест на единицата. Ако искате да научите повече за писането на тест на единица, можете да разгледате тази статия, където създавам играта на живота, използвайки разработена с тест разработка.

Не съдържа дублиране

Дублирането е основният враг на добре проектирана система. Той представлява допълнителна работа, допълнителен риск, добавя ненужна сложност.

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

Да предположим, че в момента имаме в приложението си стандартна секция за чат. Появява се ново изискване и сега искаме да внедрим нов тип чат - чат на живо. Чат, който трябва да съдържа съобщения с максимален брой 20 знака и този чат ще изчезне, когато отхвърлим изгледа на чата.

Този чат ще има същите изгледи като настоящия ни чат, но ще има няколко различни правила:

  1. Мрежовата заявка за изпращане на съобщения за чат ще бъде различна.

2. Съобщенията в чата трябва да са кратки, не повече от 20 знака за съобщение.

3. Съобщенията в чата не трябва да остават в нашата локална база данни.

Да предположим, че използваме MVP архитектура и в момента се справяме с логиката за изпращане на съобщения за чат в нашия презентатор. Нека се опитаме да добавим нови правила за нашия нов тип чат, наречен live-chat.

Наивно изпълнение би било следното:

Но какво ще стане, ако в бъдеще ще имаме много повече видове чат?
Ако продължим да добавяме, ако иначе проверяваме състоянието на нашия чат във всяка функция, кодът ще стане разхвърлян трудно за четене и поддържане. Освен това едва ли може да се провери и проверката на състоянието ще бъде дублирана в целия обхват на презентатора.

Тук се използва шаблонът на шаблона. Шаблонът на шаблона се използва, когато имаме нужда от множество реализации на алгоритъм. Шаблонът се дефинира и след това се надгражда с допълнителни варианти. Използвайте този метод, когато повечето подкласове трябва да прилагат същото поведение.

Можем да създадем протокол за Chat Presenter и отделяме методи, които ще бъдат реализирани по различен начин от конкретни обекти във фазите на Chat Presenter.

Вече можем да направим нашия презентатор да съответства на IChatPresenter

Нашият презентатор сега обработва изпращането на съобщения чрез извикване на общи функции вътре в себе си и делегира функциите, които могат да бъдат реализирани по различен начин.

Сега можем да предоставим Създаване на обекти, които съответстват на фазите на презентация и да конфигурираме тези функции въз основа на техните нужди.

Ако използваме инжектиране на зависимост в контролера ни, сега можем да използваме повторно един и същ контролер в два различни случая.

С помощта на дизайнерски модели можем наистина да опростим нашия iOS код. Ако искате да знаете повече за това, следващата статия предоставя допълнително обяснение.

изразителен

По-голямата част от разходите за софтуерен проект са в дългосрочна поддръжка. Писането на лесен за четене и поддържане код е задължително за разработчиците на софтуер.

Можем да предложим по-изразителен код, като използваме добър тест за назоваване, използване на SRP и писане.

наименуване

Номер едно нещо, което прави кода по-изразителен - и той е именуване. Важно е да напишете имена, които:

  • Разкрийте намерение
  • Избягвайте дезинформация
  • Лесно се търсят

Когато става дума за именуване на класове и функции, добър трик е да използвате съществително или съществително израза за класове и потребителски глаголи или имена на глаголни фрази за методи.

Също така, когато използвате различни дизайнерски модели понякога е добре да добавите имената на шаблони като Command или Visitor в името на класа. Така че читателят веднага ще разбере какъв модел се използва там, без да има нужда да чете целия код, за да разбере за това.

Използване на SRP

Друго нещо, което прави кода изразителен, е използването на принципа за единична отговорност, който беше споменат по-горе. Можете да изразите себе си, като запазите функциите и класовете си малки и с една единствена цел. Малките класове и функции обикновено са лесни за назоваване, лесни за писане и лесни за разбиране. Функцията трябва да служи само за една цел.

Тест за писане

Писането на тестове също носи много яснота, особено когато се работи с наследен код. Добре написаните единични тестове също са изразителни. Основна цел на тестовете е да действат като документация чрез пример. Някой, който чете нашите тестове, трябва да може да получи бързо разбиране за какво е клас.

Минимизирайте броя на класовете и методите

Функциите на даден клас трябва да остават кратки, функцията трябва винаги да изпълнява само едно. Ако дадена функция има твърде много редове, това може да е така, че тя изпълнява действия, които могат да бъдат разделени на две или повече отделни функции.

Добър подход е да преброите физическите линии и да се опитате да се стремите към максимум четири до шест реда функции, в повечето случаи всичко, което надхвърля този брой линии, може да стане трудно за четене и поддържане.

Добра идея в iOS е да нарежете конфигурационните обаждания, които обикновено правим при функциите viewDidLoad или viewDidAppear.

По този начин всяка от функциите ще бъде малка и поддържаема, вместо една функция на каша viewDidLoad. Същото трябва да важи и за делегата на приложения. Трябва да избягваме да хвърляме всяка конфигурация ondidFinishLaunchingWithOptions метод и отделни конфигурационни функции или дори по-добри конфигурационни класове.

С функциите е малко по-лесно да измерим дали го поддържаме дълъг или къс, в повечето случаи можем просто да разчитаме на преброяване на физическите линии. С класовете използваме различна мярка. Ние броим отговорностите. Ако даден клас има само пет метода, това не означава, че класът е малък, може да се окаже, че той има твърде много отговорности само с тези методи.

Известен проблем в iOS е големият размер на UIViewControllers. Вярно е, че чрез дизайна на контролера за ябълкови изгледи е трудно тези обекти да служат на една единствена цел, но трябва да опитаме всичко възможно.

Има много начини да направя UIViewControllers малки моето предпочитание да използвам архитектура, която има по-добро разделяне на проблемите като VIPER или MVP, но това не означава, че не можем да го направим и по-добър в Apple MVC.

Опитвайки се да отделим колкото се може повече проблеми, можем да достигнем доста приличен код с всяка архитектура. Идеята е да се създадат еднозначни класове, които могат да служат като помощници на контролерите на изглед и да направят кода по-четим и тест.

Някои неща, които могат да бъдат избегнати просто без извинение от гледна точка на контролерите, са:

  • Вместо да пишете мрежов код директно, трябва да има NetworkManager клас, който да отговаря за мрежовите повиквания
  • Вместо да манипулираме данни в контролерите на изглед, можем просто да създадем DataManager клас, който да отговаря за това.
  • Вместо да играем с низовете UserDefaults в UIViewController, можем да създадем фасада над това.

В заключение

Считам, че трябва да съставим софтуер от компоненти, които са точно именувани, опростени, малки, отговорни за едно нещо и за многократна употреба.

В тази статия обсъдихме четири правила за опростен дизайн от Кент Бек и дадохме практически примери за това как можем да ги реализираме в среда за разработка на iOS.

Ако ви хареса тази статия, не забравяйте да ръкопляскате, за да покажете вашата подкрепа. Следвайте ме, за да прегледате още много статии, които могат да изведат вашите умения за iOS Developer на следващо ниво.

Ако имате въпроси или коментари, не се колебайте да оставите бележка тук или да ми изпратите имейл на arlindaliu.dev@gmail.com.