Как можно улучшить архитектуру кода, чтобы избежать нарушений капсуляции?

Улучшение архитектуры кода для предотвращения нарушений инкапсуляции (капсуляции) — важный шаг к созданию надёжной, гибкой и масштабируемой системы. Инкапсуляция предполагает, что внутреннее состояние объекта должно быть защищено от прямого вмешательства и управляться только через чётко определённый интерфейс.

Нарушения капсуляции происходят, когда:

  • внешние объекты получают доступ к внутренним полям напрямую;

  • внутренняя реализация «просачивается» наружу;

  • изменение одного класса тянет за собой изменения в других (tight coupling).

Ниже — стратегии, позволяющие укрепить капсуляцию и улучшить архитектуру.

🔐 1. Скрывайте поля (private, val) и предоставляйте доступ через методы

  • Не делайте поля public, особенно var.

  • Используйте private, internal или protected по необходимости.

  • В Kotlin предпочтительнее val вместо var, если значение не должно изменяться.

class User(private val name: String, private val age: Int) {
fun isAdult(): Boolean = age >= 18
}

🛡 2. Используйте интерфейсы и абстракции

  • Зависимости внедряйте через абстракции, а не через конкретные реализации.

  • Это позволяет изолировать слои и модули.

interface UserRepository {
fun getUser(id: String): User
}
class UserRepositoryImpl(private val api: UserApi) : UserRepository {
override fun getUser(id: String): User = api.fetch(id)
}

В UI слое используйте только UserRepository, а не UserRepositoryImpl.

🏛 3. Соблюдайте принципы SOLID

Особенно:

  • S (Single Responsibility) — каждый класс должен отвечать только за одну часть логики.

  • D (Dependency Inversion) — модули верхнего уровня не зависят от модулей нижнего уровня; и те и другие зависят от абстракций.

⚠️ 4. Избегайте «анемичных» моделей

Анемичная модель — это класс, содержащий только данные (var-поля), но без логики. Такой класс не инкапсулирует поведение, что приводит к «утечке» логики в другие слои.

❌ Плохо:

class Order(var items: List<Item>, var status: String)
fun cancel(order: Order) {
order.status = "CANCELLED"
}

✅ Лучше:

class Order(private val items: List<Item>) {
private var status: String = "NEW"
fun cancel() {
status = "CANCELLED"
}
fun getStatus(): String = status
}

Теперь логика сосредоточена внутри самого объекта, и внешние классы не могут менять status напрямую.

5. Изолируйте внутренние реализации

  • Используйте internal (в Kotlin) или package-private в Java для скрытия вспомогательных классов.

  • Не раскрывайте детали слоя data или network в UI или domain.

6. Выделяйте слои и DTO (Data Transfer Objects)

  • Не передавайте доменные модели напрямую в UI или через сеть.

  • Используйте мапперы, чтобы избежать «протечки» слоёв друг в друга.

// Domain
data class User(val name: String, val isPremium: Boolean)
// UI
data class UserUiModel(val displayName: String, val iconRes: Int)
// Mapper
fun User.toUiModel(): UserUiModel { ... }

7. Используйте copy() вместо прямого изменения

Kotlin data class позволяет безопасно изменять состояния через copy, избегая мутирования исходного объекта:

val updatedUser = user.copy(name = "Alice")

8. Проверяйте доступность данных только через методы

  • Не публикуйте внутренние поля, даже если они val.

  • Предоставляйте только нужную информацию в безопасной форме:

private val \_items = mutableListOf<Item>()
val items: List<Item> get() = \_items.toList()

9. Соблюдайте принцип Tell, Don’t Ask

Вместо того чтобы «запрашивать» данные и действовать на основе них, сообщайте объекту, что нужно сделать:

❌ Плохо:

if (order.status == "NEW") {
order.status = "PAID"
}

✅ Хорошо:

order.pay()

Это уменьшает количество логики вне объекта и защищает внутреннее состояние.

10. Старайтесь избегать var вообще

  • Чем меньше изменяемого состояния, тем выше надёжность и инкапсуляция.

  • Используйте val и неизменяемые коллекции (List, Map) по умолчанию.

✅ Итого: чтобы сохранить инкапсуляцию

  • Скрывайте детали реализации (вплоть до видимости классов и функций).

  • Упаковывайте данные и поведение вместе.

  • Не позволяйте другим объектам изменять состояние напрямую.

  • Следите, чтобы изменение в одной части системы не заставляло менять другую.

  • Избегайте утечки зависимостей между слоями.

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