СОДЕРЖАНИЕ: 00:00:00 - про меня ;) 00:01:21 - обзор проекта с Clean Architecture 00:03:18 - чистая архитектура на диаграмме 00:05:47 - создаем Storage компонент 00:14:14 - Storage компонент на диаграмме 00:16:54 - про раздельные модели на диаграмме 00:21:08 - делаем раздельные модели в коде на Android 00:28:54 - чистим и улучшаем код 00:32:40 - подводим итого
private val userRepository by lazy {UserRepositoryImpl(userStorage = SharedPrefUserStorage(context = applicationContext))} в активити. На следующем уроке скажут.
Видео - огонь!!!) Так детально и доходчиво пожалуй в ру сегменте мало кто рассказывает. Жаль про мапперы до конца тут не рассмотрено. А не лучше ли мапперы располагать в отдельном пакете на уровне пакетов слоев (дата, домен, презентейшн)? Ведь по факту они особо к конкретному слою не принадлежат и связаны с несколькими слоями.
Нет. Мапперы связаны только с одним слоем. Например, вы мапите из дата в домен. В этом случае домен не должен ничего знать про маперы, он просто получает нужные данные и все. То есть маперы все лежат в дата. Если вынести в отдельный модуль, то есть риск, что это начнуть неверно переиспользовать и все пойдет боком.
Ну так если ты в одной модели поменяешь поля, то придётся менять и в другой модели? Где тогда тут независимость? А если была одна общая модель, то поменял бы в одном месте и всё. Другой пример, когда я хочу добавить ещё одно поле к User, например, адрес. Если у нас есть две модели, то я добавляю в одну, добавляю в другую, потом дописываю маппинг и дописываю сохранение. В случае если у меня одна общая модель, то я добавляю в одной модели и дописываю сохранение - всё. Ради только "независимости" не вижу смысла это делать.
Вы рассматриваете вариант небольшого приложение, где модели, которые пришли, в том же виде выводятся на экран, в этом случае такой маппинг действительно не нужен. Но как только у вас появятся поля, которые есть например в базе данных но их нет в domain модели, то как будите с таким работать? Скорей всего такие модели со временем превратятся в свалку, где невозможно будет разобраться, какие поля и где используются. В более больших проектах, такое разделение, да, требует больше кода, но по итогу защищает код от ошибок, делает код более простым и предсказуемым. Дело далеко не только в независимости слоев, дело в том, что-бы пользовательский код сделать максимально простым, без необходимости думать о том какие поля и как необходимо их использовать.
@@ohjelmistokehittaja4446 Спасибо огромное за Ваш труд. Можно один глупый вопрос? Обязательно создавать переменную - userStorage: SharedPrefUserStorage? Или можно сразу инстанс передать? - private val userRepository by lazy { UserRepositoryImplementation(SharedPrefsUserStorage(context = applicationContext)) }
Тимофей, привет! Подскажи пожалуйста, у меня в классе App, который наследует application и идет в name в манифесте, есть логика по изменению языка всего приложения и ночной темы, в рамках клин архитектуры и архитектуры в целом мне нужно эту логику из App куда-то переносить? Или пусть лежит себе в App, она не очень мудреная, спасибо за ответ
А кто мне доступно объяснит зачем везде пихают мапперы, и почему дата у всех реализует домен? Если мы говорим про разделение, то давайте четко и разделять, без всяких интерфейсов, просто классами все прекрасно разделяется и работает, а интерфейсы - это про другое. Сначала говорим, что Data слой должен быть максимально простой и без логики, а потом туда запихиваем всю логику приложения (реализация domain - она почему-то в data оказывается и никого это не смущает). Entity можно использовать везде (для отображения, обычно еще нужно что-то отформатировать, но так никто не мешает работать с 1 набором моделей и без всяких маперов), clean - не про них вообще. Давайте не писать код ради кода и усложнять этот самый код, затягивая сроки написания чистого и красивого кода.
Мапперы нужны, что-бы развязать логику и дают возможность каждому компоненту работать со своей моделью, и соответственно делать с ней все, что будет нужно не заботясь об остальных. А добавлять или не добавлять маперы, это уже дело конкретного проекта. Исходя из моего опыта, на длинной дистанции это всегда оправдано. По поводу "почему дата у всех реализует домен" - просто это наиболее частый случай. Реализацию можно писать и в других модулях, да и доменов и дат может быть много в приложении. Насчет логики домена в дате, не понял вас, какая логика из домена у нас оказалась в дате? В нашем случае логики, как таковой, нет вообще, поэтому и юз кейсы простые. Нужно понимать, что это простой проект, просто для примера ;), запихать сюда кучу логики и взорвать мозг студентам не цель данного видео))))).
В конце видео вы анонсировали про мапперы, какие варианты есть с их достоинствами и недостатками. Не могу найти видео на эту тему, его нет? Это очень интересная тема, потому-что моделек уже 2 (хотя для UI тоже бы завести) и иной раз не понятно где хранить маппер который из одного слоя в другой мапит.
Да, отдельного видео по этой теме не было. Я рекомендую использовать kotlin extensions и хранить мапперы либо в отдельном пакете, например mappers, либо рядом с самими модельками.
Привіт В мене є наступне питання Слой Data як я розумію відповідає суто за роботу з даними, чи то локальні чи на сервері.Значить якщо мені треба зробити певні операції з цими даними то їх я вже виконую в UseCase класах? І кидаю готові дані після певних операції на ViewModel?
Потому, что это очень частая ошибка делать одинаковые модели. Например, модель User может содержать id, но при добавлении новых данных у вас еще нет id, и выходит, что вы вынуждены делать поле id nullable, поэтому сразу показал развернутый вариант. Если все поля один в один совпадают, то делать вторую модель не нужно.
@@TimofeyKovalenko Есть еще вопрос по viewModelfactory если вас не затруднит Есть такой вариант передачи параметра в конструктор viewModel ________________________________________ class WeatherViewModel(val repository: WeatherRepository) : ViewModel() { constructor() : this(WeatherRepository(ApiClient().getClient().create(ApiServices::class.java))) __________________________________________ вторичный конструктор который сробатывает после первичтоно Настоклько это хуже фабрики и хуже ли вообще?
С точки зрения клин архитектуры - это одно и тоже. Но если почитать умные книжки, то есть какие-то очень тонкие отличия. Так же часто считается, что Use Case содержит только один публичный метод, а Interactor может иметь несколько. Тут нужно понимать, что клин архитектуру можно сделать вообще без юз кейсов или репозиториев, используя другие паттерны и принципы. То есть это все наполнение, а не основа основ клина.
Тимофей, спасибо за подробные и качественные разборы Clean Architecture! Есть вопрос насчет ответственности репозитория. Вы сказали, что именно репозиторий решает, в какие хранилища сходить - в интернет, в базу данных, в Shared Preferences или во все одновременно. Но представьте ситуацию, что в рамках одной сущности (например, Фильм) есть два разных сценария: - На экране А нужно скачать фильмы из сети, сложить в базу данных и отобразить на экране. - На экране Б нужно скачать фильмы из сети и сразу отобразить на экране, без складывания в базу данных. В таком случае, необходимо сделать два разных метода в репозитории? Тогда как их именовать? Не логичнее было бы сделать несколько репозиториев, ответственных за конкретное хранилище, а на стороне конкретного UseCase-а обращаться к нужным репозиториям?
В данном случае я бы все же сделал 2 разных юз кейса и разные методы в репозитории. Так в разы понятнее, какой экран, что делает и логика в разы проще и линейнее. Да и это же 2 разных варианта использования вашего приложения, которые заложены логикой, поэтому 2 юз кейса вполне логично. Возможность подкидывать разные реализации в основном нужна для того, что бы если меняется приложение/требования/источники данных, можно было сделать новые реализации по минимум затрагивая текущий код. То есть, по сути, это делается для более гибкого изменения и расширения кода, а не для того, что бы строить вокруг этого логику.
@@TimofeyKovalenko спасибо большое за ответ! Да, насчет 2х юз кейсов сомнений не было, с ними всё предельно понятно, 2 сценария = 2 юз кейса. А вот с репозиториями не до конца понятно. Представьте, что сценарий номер 1 уже реализован, а теперь нужно реализовать сценарий 2. Вроде бы, всё обращение к данным у нас уже реализовано - в сеть ходить умеем, в базу складывать умеем, но почему-то требуется дописать код в слое data, добавив новый метод в репозиторий 🤔 А вот если разбить репозитории по источнику данных, например, FilmNetworkRepository, FilmStorageRepository, тогда юз кейсы смогут по собственному усмотрению пользоваться разными репозиториями в разных ситуциях, и для реализации 2го сценария не пришлось бы редактировать слой data 🤔 Может быть, я не до конца понимаю ответственность репозитория, но мне кажется, что в случае двух разных методов репозитория его реализация и набор методов косвенно становятся зависимы от бизнес-логики, что может привести к его бесконечному разрастанию 🤔
Если я правильно понял, то ваш класс FilmNetworkRepository будет не только в сеть ходить, но и сохранять данные в базу, правильно? Если да, то такое разделение очень запутанно и странно выглядит. Для локальной базы у вас есть FilmStorageRepository. С такой логикой получается, что вы в юз кейсе должны получить данные из FilmNetworkRepository, а затем сохранить их, используя FilmStorageRepository. А в юз кейсе, где нужно только локальные данные читать, обратиться только к FilmStorageRepository. Но, это не совсем правильно, так как данными должен управлять репозиторий, а не UseCase. Тоесть если делаете такое разделение, то не нужно мешать логику внутри. По поводу бесконечного разрастания репозитория, это не проблема, если для разных сущностей вы используете разные репозитория. Очень сомневаюсь, что у вас будет так много вариаций работы с данными, что это заставить писать десятки методов. К тому же очень вряд ли, что методы работы с локальной базой, и методы работы с нетворк у вас будут одинаковые, я имею ввиду их количество и функционал. В реальном проекте, как правило так не бывает. Поэтому я бы рекомендовал все же иметь два метода или один с параметром, который задает конфиг работы с данными. Плюс, я бы сделал интерфейс Storage и от него 2 наследника NetworkStorage и LocalStorage. Эти объекты можно использовать в репозитории (подавать в конструктор естественно), и тем самым вынести работу с чтением/сохранением, и в репозитории оставить только логику работы с данными и манипулировать источниками.
Есть вопрос - а как с помощью такого подхода реализовать получение данных из сети? Ведь работа с сетью будет в модуле дата, но методом get в usecase мы вернуть данные из сети не сможет. Дата про presentation нечего не знает чтобы передать туда ответ..
Посмотрел 3 видео, очень понятно, за пол часа полность переделал свое приложение по клину. У меня уже было какое то подобие, mvvm+repository, но вынес логику из вьюмодели в usecase. Теперь приятно глазу и понимаю, что читать и маштабировать приложение намного легче.Спасибо!
@@TimofeyKovalenko извините, я что-то спутал. У меня есть интерфейс DataProvider, у которого есть имплементации для CSV, XML и JDBC. Я инициализирую для каждой модели Storage свой DataProvider. Затем получается я должен передавать это в отдельный Repository для каждой конкретной модели в Domain, и передать в UseCase, всё верно?
Вы говорите, что слой domain ни от кого не зависит. Но при этом когда описываете setName 28:46 вы импортируете data.storage.User. Получается если вы измените например имя поля в data.storage.User нужно будет все равно лезть в domain исправлять.
Вы наверное, что-то не досмотрели. Domain модуль не имеет зависимости Data. То есть даже, если бы мы очень захотели использовать data.storage.User в домене, то не смогли бы. Возможно вы спрашиваете про то, что мы использует модель storage в реализации репозитория. Тут проблем нет, так как Storage ни о ком не знает, и те кто его использует, естественно получает Storage модели, и дальше уже маппит или как-то еще использует. Storage это часть более низкого уровня чем репозиторий, поэтому репозиторий и имеет доступ к Storage моделям.
Да, назначение даты как раз и заниматься хранением данных, поэтому в большинстве случаев нет необходимости его выносить. Но если у вас модуль массивный либо используется разными системами, то конечно можно вынести.
Благодарю вас, Тимофей, за вашу работу! Редкость получать столь уникальные вещи! Ребят, старайтесь побольше практиковаться, а именно на фоне писать какое-то другое свое приложение с использованием каких-то вещей, о которых было рассказано в самом видео. Не стоит код переписывать!
Огромное Вам спасибо за контент! В данный момент уже занимаюсь на курсах по андроид разработке. Дошёл до "MVVM", "Dagger-2", "Hilt". Преподаватели стараются, объясняют, но, видимо, я туповат - приходится искать уроки с более простой подачей. Недавно открыл для себя Ваш канал. Теперь жалею, что занимаюсь не на Ваших курсах.
То есть получается дата и домен работают с отдельными своими типами объектов, но эти типы связаны логикой приложения. А маперы в репозитории приводят один тип к другому для передачи между слоями?
Спасибо за урок. Но у меня есть сомнений. В слое domain есть модел UsernameModel и при добавление еще одно поле в конструктор данного класса в Storage появится ошибка. Это правильно или нет?
Да, все тут верно. Просто домена модель это один API, storage это совсем другой API. Если задача подразумевает и там и там менять, то да, нужно поменять в двух местах. А если вы будите одну и туже модель использовать для UI и для Storage, то во первых модель может обрасти специфическими данными, которые нужны только кому-то одному, а во вторых при изменении модели вы сразу измените совершенно разные компоненты. Архитектура это не про то, что-бы быстро накидать код, а про удобство на длинной дистанции.
Если логика поменялась, понятное дело зависимости тоже нужно менять. Независимость достигается тем, что каждая часть имеет свой API(свои методы и ентити) и их реализация не зависит от хотелок того, кто будет использовать эту логику. То есть domain не зависит от потребителей, а потребитель всегда подчиняется правилам API. К тому же, если у вас что-то поменялось в домене, вам это что-то требуется использовать - показать на экране или еще как то - тут никакая независимость не поможет))). В этом случае правильнее не изменять текущую логику, а добавить новую.
Спасибо за уроки! А данный код не скинете например на гитхаб? Также не понял название методов с приставкой map. Логичнее например userToUserName(), а не mapToDomain() ?
В котлине, RX и в многих других фреймворках/языках есть стандартные методы, которые как раз называются map и делают конвертацию данных. Также это просто общеупотребительное слово для подобных операций и такое название будет понятно большинству программистов. userToUserName как раз таки не говорит о том, что будет делаться, вообще названия методов лучше начинать с глагола.
По поводу кода нет, никогда не скидываю студентам код на котором показываю примеры, иначе толку от лекции нет. Понять можно только если самому написать.
@@TimofeyKovalenko )) такие схемы и показываются на UML) ! не нестоит ,просто в начале видео покажи стрелки зависемости,наследование, реализаций ,и это на 100 хватит в этом видео
получается если я сделаю RoomUserStorage у которого свои entity с аннотациями, я должен внутри storage скоупа мапить из UserEntity(RoomUserStorage) -> User(UserStorage) и прокинуть в Repository который смапит в UserName(Domain)?
Storage не должен знать ни о чем вокруг него, маппинг делаете за пределами storage. То есть репозиторий получит entity из Storage и далее должен будет их замапить.
@@TimofeyKovalenko нет смотрите , UserStorage - интерфейс, и репозиторий не знает ничего о реализации ,там может быть NetworkUserStorage , RoomUserStorage , SharedPrefUserStorage - и у каждого будут свои сущности с аннотациями, может и нет), также могут присутствовать лишние поля которые не нужны дальше. Вы говорите "репозиторий получит entity из Storage и далее должен будет их замапить." - это же означает, что теперь репозиторий хоть работает с интерфейсам, но все же жестко зависит от Room имплементации, из за мапинга. Поэтому может каждая имлементация внутри Storage скоупа должна смапить к обобщенной модельки которая будет независимой(без аннотации итд) , и прокидать в репозиторий который будет мапить Storage модельку в Domain модель - и тут репо теперь не знает о реализации надеюсь смог объяснить :)
Если модели разные, то это будут совершенно разные интерфейсы. Репозиторий просто будет работать с каждым по отдельности. Вы пытаетесь сделать так, что-бы Storage подстроился под требования репозитория, но так делать не стоит. Да и выглядит так, что вы выносите логику репозитория в Storage. На практике, практически никогда не делают один интерфейс для нетворка и локального хранилища, это не удобно и сильно ограничивает вас в дальнейшем,
@@TimofeyKovalenko да вы правы) , согласен что не делают один интерфейс для нетворка и локального хранилища вот последний вопрос, извиняюсь). У нас есть условно 4 репозитория которые зависят от LocalDataStorage и у него была RealmLocalDataStorageImpl или какой то другая impl не важно, через некоторое время мы мигрировали на RoomLocalDataStorageImpl и это все внутренности LocalDataStorage о котором не должны знать клиенты - то есть те которые зависят от этого стораджа. подскажите какую модельку должен предоставить LocalDataStorage чтобы каждый клиент не ломался от нашей миграции?