Лідери думок

Реалізація принципів SOLID у розробці Android-додатків

mm
Додайте Unite.AI до бажаних джерел у Google

Розробка програмного забезпечення – це акт творчості, і розробка Android-додатків не є винятком. Це не тільки про те, щоб зробити щось працюючим. Це про проектування застосунків, які можуть розвиватися, адаптуватися та залишатися керованими з часом.

Як розробник Android, який зіштовхнувся з безліччю архітектурних проблем, я виявив, що дотримання принципів SOLID може перетворити навіть найбільш заплутані кодові бази в чисті системи. Це не абстрактні принципи, а результативні та повторювані способи написання надійного, масштабованого та підтримуваного коду.

Ця стаття надасть уявлення про те, як принципи SOLID можуть бути застосовані до розробки Android-додатків через реальні приклади, практичні техніки та досвід команди Meta WhatsApp.

Поняття принципів SOLID

Принципи SOLID, запропоновані Робертом С. Мартіном, – це п’ять принципів проектування для об’єктно-орієнтованого програмування, які гарантують чисту та ефективну архітектуру програмного забезпечення.

  • Принцип єдиної відповідальності (SRP): Клас повинен мати тільки одну причину для зміни.
  • Принцип відкритості/закритості (OCP): Програмні сутності повинні бути відкритими для розширення, але закритими для модифікації.
  • Принцип заміщення Ліскова (LSP): Підкласи повинні бути заміщуваними для їхніх базових класів.
  • Принцип сегрегації інтерфейсу (ISP): Інтерфейси повинні бути клієнт-специфічними та не повинні примушувати реалізовувати незастосовувані методи.
  • Принцип інверсії залежностей (DIP): Високорівневі модулі повинні залежати від абстракцій, а не від низькорівневих модулів.

Інтегруючи ці принципи в розробку Android-додатків, ми можемо створити застосунки, які легше масштабувати, тестувати та підтримувати.

Принцип єдиної відповідальності (SRP): Упрощення відповідальності

Принцип єдиної відповідальності – це основа написання підтримуваного коду. Він говорить про те, що кожен клас повинен мати тільки одну відповідальність. Поширений анти-паттерн – це розгляд діяльності або фрагментів як “бог-класів”, які обробляють відповідальність, починаючи від відображення інтерфейсу, потім отримання даних, обробки помилок тощо. Цей підхід робить тестування та підтримку кошмаром.

З SRP окремі проблеми розділяються на окремі компоненти: наприклад, у застосунку для новин, створити або прочитати новини.


class НовиниРепозиторій {
fun отриматиНовини(): Список {
// Обробляє логіку отримання даних
}
}

class НовиниViewModel(приватний val новиниРепозиторій: НовиниРепозиторій) { fun завантажитиНовини(): LiveData<Список> { // Керує станом інтерфейсу та потоком даних } }

class НовиниАктивність : AppCompatActivity() { // Обробляє тільки відображення інтерфейсу }

 

Кожен клас має тільки одну відповідальність; тому його легко тестувати та змінювати без побічних ефектів.

У сучасній розробці Android SRP переважно реалізується разом з рекомендуваною архітектурою за допомогою Jetpack. Наприклад, логіка, пов’язана з маніпуляцією даними, може знаходитися всередині ViewModel, тоді як діяльності або фрагменти повинні лише турбуватися про інтерфейс та взаємодію. Отримання даних може бути делеговано окремому репозиторію, як від місцевих баз даних, таких як Room, так і мережевих шарів, таких як Retrofit. Це знижує ризик розширення класів інтерфейсу, оскільки кожен компонент отримує тільки одну відповідальність. Одночасно ваш код буде значно легше тестувати та підтримувати.

Принцип відкритості/закритості (OCP): Проектування для розширення

Принцип відкритості/закритості говорить про те, що клас повинен бути відкритим для розширення, але не для модифікації. Це більш розумно для застосунків Android, оскільки вони постійно оновлюються та додають нові функції.

Найкращий приклад того, як використовувати принцип OCP у застосунках Android, – це інтерфейси та абстрактні класи. Наприклад:


interface СпосібОплати {
fun обробитиОплату(сума: Double)
}

class ОплатаКредитноюКартою : СпосібОплати { override fun обробитиОплату(сума: Double) { // Реалізація для оплати кредитною картою } }

class ОплатаPayPal : СпосібОплати { override fun обробитиОплату(сума: Double) { // Реалізація для оплати PayPal } }

 

Додавання нових методів оплати не потребує змін існуючих класів; це потребує створення нових класів. Саме тут система стає гнучкою та може бути масштабована.

У застосунках, створених для пристроїв Android, принцип відкритості/закритості досить корисний, коли мова йде про перемикачі функцій та конфігурації, прийняті динамічно. Наприклад, якщо ваш застосунок має базовий інтерфейс AnalyticsTracker, який повідомляє про події різних сервісів аналітики, Firebase та Mixpanel, а також власних внутрішніх трекерів, кожен новий сервіс може бути доданий як окремий клас без змін існуючого коду. Це тримає ваш модуль аналітики відкритим для розширення – ви можете додавати нових трекерів – але закритим для модифікації: ви не переписуєте існуючі класи кожного разу, коли додаєте новий сервіс.

Принцип заміщення Ліскова (LSP): Забезпечення заміщення

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

Наприклад, застосунок для малювання:


abstract class Фігура {
abstract fun розрахуватиПлощу(): Double
}

class Прямокутник(приватний val ширина: Double, приватний val висота: Double) : Фігура() { override fun розрахуватиПлощу() = ширина * висота }

class Коло(приватний val радіус: Double) : Фігура() { override fun розрахуватиПлощу() = Math.PI * радіус * радіус }

 

Обидва Прямокутник та Коло можуть бути замінені будь-яким іншим взаємозамінно без системної відмови, що означає, що система гнучка та слідує LSP.

Розгляньте підкласи RecyclerView.Adapter у Android. Кожен підклас адаптера розширюється від RecyclerView.Adapter<VH> та перевизначає основні функції, такі як onCreateViewHolder, onBindViewHolder та getItemCount. RecyclerView може використовувати будь-який підклас взаємозамінно, якщо ці методи реалізовані правильно та не порушують функціональність вашого застосунка. Тут LSP підтримується, і ваш RecyclerView може бути гнучким для заміщення будь-якого підкласу адаптера за бажанням.

Принцип сегрегації інтерфейсу (ISP): Стримані та зосереджені інтерфейси

У великих застосунках часто визначаються інтерфейси з надто великою відповідальністю, особливо навколо мережевих або даних сховищ. Замість цього розділіть їх на менші, більш цілевказані інтерфейси. Наприклад, інтерфейс ApiAuth, відповідальний за кінцеві точки аутентифікації користувача, повинен бути відокремлений від інтерфейсу ApiPosts, відповідального за публікації в блозі або соціальних мережах. Це розділення запобігатиме ситуаціям, коли клієнти, які потребують тільки методів, пов’язаних з публікаціями, будуть примушені залежати від та реалізовувати виклики аутентифікації, тим самим тримаючи ваш код, а також покриття тестів, стриманішим.

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

Наприклад, замість того, щоб мати один великий інтерфейс, який представляє дії користувача, розгляньте код на Kotlin:


interface Аутентифікація {
fun вход()
fun вихід()
}

interface УправлінняПрофайлом { fun оновитиПрофайл() fun видалитиАкаунт() }

 

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

Принцип інверсії залежностей (DIP): Абстрагування залежностей

Принцип інверсії залежностей сприяє декуплінгу, забезпечуючи залежність високорівневих модулів від абстракцій, а не від конкретних реалізацій. Цей принцип ідеально відповідає сучасним практикам розробки Android, особливо з фреймворками ін’єкції залежностей, такими як Dagger та Hilt.

Наприклад:


class UserRepository @Inject constructor(приватний val apiService: ApiService) {
fun отриматиДаніКористувача() {
// Отримує дані користувача з абстракції
}
}

 

Тут UserRepository залежить від абстракції ApiService, роблячи його гнучким та тестовим. Цей підхід дозволяє нам замінити реалізацію, наприклад, використовувати сервіс-мок під час тестування.

Фреймворки, такі як Hilt, Dagger та Koin, сприяють ін’єкції залежностей, забезпечуючи спосіб постачання залежностей до компонентів Android, усуваючи необхідність їхньої прямої інстанціації . У репозиторії, наприклад, замість інстанціації реалізації Retrofit, ви ін’єктуєте абстракцію – наприклад, інтерфейс ApiService. Таким чином, ви можете легко змінити мережеву реалізацію – наприклад, на сервіс-мок для локального тестування – і не потребуватимете змінювати нічого в коді репозиторію. У реальних застосунках ви можете знайти класи, анотовані @Inject або @Provides, щоб надавати ці абстракції, тим самим роблячи ваш застосунок модульним та тестовим.

Практичні переваги принципів SOLID

Застосування принципів SOLID у розробці Android-додатків дає відчутні переваги:

  1. Поліпшена тестованість: Зосереджені класи та інтерфейси роблять його легше писати одиницеві тести.
  2. Поліпшена підтримуваність: Чіткий поділ відповідальності спрощує налагодження та оновлення.
  3. Масштабованість: Модульні дизайни дозволяють безперешкодно додавати нові функції.
  4. Співробітництво: Добро структурований код сприяє командній роботі та скорочує час ознайомлення нових розробників.
  5. Оптимізація продуктивності: Стрімкі, ефективні архітектури мінімізують зайву обробку та використання пам’яті.

Реальні застосування

У застосунках з великою кількістю функцій, таких як електронна комерція або соціальні мережі, застосування принципів SOLID може суттєво зменшити ризик регресій кожного разу, коли додається нова функція або сервіс. Наприклад, якщо новий вимір вимагає процесу покупки в застосунку, ви можете ввести окремий модуль, який реалізовуватиме необхідні інтерфейси (Оплата, Аналітика) без зміни існуючих модулів. Такий модульний підхід, керований SOLID, дозволяє вашому застосунку Android швидко адаптуватися до вимог ринку та утримувати кодову базу від перетворення на спагетті з часом.

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

Висновок

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

Хороший код – це не тільки про те, щоб зробити щось працюючим – це про створення системи, яка може продовжувати працювати та розвиватися з часом. Приймаючи принципи SOLID, ви не тільки пишете кращий код, але також створюєте застосунки, які є радістю розвивати, масштабувати та підтримувати.

Фархана - експерт з розробки мобільних застосунків, яка успішно створила численні мобільні застосунки з нуля. Вона провела навчання з розробки Android для урядових спеціалістів з інформаційних технологій у Бутані, наставляла багатьох нових учасників та очолювала команди для досягнення успіху разом.