Как да приложим принципа за единична отговорност в Swift

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

Добър начин да се разбере наистина отговорността за даден клас е мисленето в мащабируемост.

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

Въведение в принципа на единната отговорност

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

Това е един от 5-те основни принципа на софтуерния дизайн SOLID, където в обектно-ориентираното програмиране те се опитват да дефинират ръководство, за да имат по-разбираем, гъвкав и поддържан софтуер.

Тези принципи са:

  • Принцип на единната отговорност
  • Отворен / затворен принцип
  • Принцип на заместване на Лисков
  • Принцип на разделяне на интерфейса
  • Принцип на инверсия на зависимостта

Авторът на тези концепции Робърт К. Мартин (той написа една от най-важните книги в софтуерната архитектура, Чист код) говори за „Един клас трябва да има само една причина да се променя“, така че той определя отговорността като причина да да се промени.

Например, нека разгледаме модул, който компилира и отпечатва отчет. Представете си, че такъв модул може да бъде променен по две причини. Първо, съдържанието на доклада може да се промени. Второ, форматът на доклада може да се промени. Тези две неща се променят поради много различни причини; една съществена и една козметична.

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

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

Защо е важно да се определи правилно отговорността на всеки клас

Ако дефинираме класовете си, знаейки коя е тяхната отговорност в нашия проект, можем:

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

Мислене в възможността да се определят отговорностите

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

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

Нека да видим конкретен пример в Swift.

Бърз пример

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

ItemsViewController: Прост списък с елементи

Можете да видите кода в https://github.com/fedejordan/SRPExample

За да направи това, ItemsViewController използва UITableView, за да показва елементите в списък. Също така използваме подклас UITableViewCell, наречен ItemTableViewCell, за да покажем тези елементи.

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

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

Истинският проблем е в тези редове:

нека item = items [indexPath.row]

Защо проблемът е тук?

Тъй като използваме индекса UITableView, за да получим конкретния елемент в масива. Трябва да абстрахираме по някакъв начин, че изгледът използва anUITableView.

За да избегнем това, ще рефакторираме ItemViewController и ще преместим логиката на модела в друг клас, наречен ItemsInteractor.

Концепцията за взаимодействие има своя произход в архитектурата на VIPER. Както се казва в дефиницията, Interactor съдържа бизнес логиката за манипулиране на моделни обекти (Entities) за изпълнение на конкретна задача. В този случай нашият ItemInteractor отговаря за извличане на информация за всеки артикул.

Можете да получите този код в https://github.com/fedejordan/SRPExample, в клона interactor_refactor.

Както виждаме, ItemsViewController не знае нищо за модела на данни. Той просто попита Интерактора какво му е необходимо, за да привлече гледката.

С този подход можем, например, да променим елемента от тип данни за всяко друго нещо и бихме променили само ItemInteractor. Той ще извлече същата информация (не бихме променили публичните методи) и накрая ItemViewController може да продължи да работи както винаги.

Така че с този рефактор, ако искаме да променим само оформлението на нашето приложение, ние просто променяме ItemsViewController, за да използваме anUICollectionView:

Скрийншот на ItemViewController, използвайки UICollectionView

Можете да видите крайния резултат в https://github.com/fedejordan/SRPExample, collection_view_refactor клон.

Променихме ли нещо в ItemInteractor? Просто нищо. Ние просто променихме представянето на функцията.

Това също ни позволява да тестваме всичко по по-модулиран начин. Първо можем да започнем да тестваме гледката, а след това и бизнес логиката. Разбира се, че за да правим тестове правилно, трябва да направим нещо по-инжектируемо, така че можем да инициализираме модулите с подигравани класове и зависим от интерфейси или протоколи в Swift. По принцип това означава, например, не създаване на ItemInteractor вътре в ItemViewController, защото това е зависимост и трябва да се абстрахира от неговото прилагане. Но всичко това надхвърля целта на статията.

заключение

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

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

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

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

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

Благодаря ви много за четенето на публикацията!

Надявам се да ви е харесало. Можете да изпратите всяко съобщение или предложение по темите в секцията за коментари или просто да ми изпратите имейл на fedejordan99@gmail.com