Replies: 2 comments 8 replies
-
Maybe I’m missing something here but why doesn’t your domain layer just conform to TestDependencyKey and provide reasonable test and preview values then your data layer which provides the live implementation conform to DependencyKey? |
Beta Was this translation helpful? Give feedback.
-
I don't see how Clean architecture is not that over-engineering that you want to avoid. Layers over layers to get... What? To force every developer to travel through lots of files to see what even the simplest things do? Can you elaborate why you need Clean? |
Beta Was this translation helpful? Give feedback.
-
Introduction
We are starting a complete revamp of the legacy iOS application. The application is quite complex (100k+ lines of source code). Because of the significant complexity, we want to use Clean Architecture layering with TCA as the primary technology for the presentation layer in conjunction with SwiftUI. We are looking for a pragmatic way to use these technologies side by side and not over-engineer the whole solution. We want to leverage other libraries from the TCA ecosystem (such as Dependencies) on different layers. Here, I'm providing only the blueprint we are preparing for the development team.
Any feedback or ideas for improvement would be appreciated.
Structure
Modules should split the application, and each module should be divided into layers into independent packages that allow access restriction.
I will not explain all the principles of clean architecture. Many good articles are available on the web.
We are considering creating a single package with several products for each module. The structure should be the following:
The domain layer does not depend on other layers. Code has visibility only for classes/structs and protocols defined inside this package. Besides protocols for UCs, which implement business logic, there are protocols for
Repositories
. These protocols allow for the invocation of data layer functionalities, but the actual implementation of these protocols is not part of this layer.The data layer depends only on the domain layer. It should be responsible for communicating with the backend or local storage. It implements
Repositories
defined on the domain layer to provide the output of these operations to the domain layer. Typically, the data models on the data layer and domain layer do not match, so it is necessary to convert the output into the domain model to provide an understandable structure to the domain layer.The presentation layer should depend only on the domain layer. The presentation could not access the data layer, and it must be impossible to invoke any functionalities directly from the data layer. It must not be possible to import
PackageAData
inPackageAPresentation
.This is a strict, Clean Architecture approach.
Problem
On the domain layer, we defined simple UC, which will use a repository to retrieve the data. The repository implementation is not visible to the domain layer, so it is impossible to immediately provide the
liveValue
for the repository key on the domain layer. A small sample follows:On the data layer, the implementation of the
FinancialRepository
is provided.Because the implementation is not public, it needs to be there also some method which can push it to the dependency container:
On the application layer, then, we can easily set up the dependency container during app initialisation:
The presentation layer can then use the use case without problem when it runs in the simulator or on a physical device. Here is the shortened presentation layer.
As I wrote, this solution works fine for runtime but starts failing during preview.
Currently, the
FinancialRepositoryKey.liveValue
ends withfatalError
, and the preview fails.As I mentioned, the data layer should not be visible from the presentation layer, so the problem cannot be resolved using the
withDependencies
function.Potential solutions
Mock data on feature client
One solution is to mock data on the feature’s client.
I don’t like this solution because it moves some “data-related” functionality to the presentation layer and duplicates some data logic. Here, it is simple, but the logic will be more complex in the actual application, and maintaining consistency will not be easy.
Introduce the DI package
One team member has proposed one alternative. Introducing new layer DI. The package structure would be something like this:
The dependencies will be the same as in the previous model, with only one exception: The presentation layer will now depend on the DI layer, too.
The DI layer depends on domain and data layers.
The implementation of UC would not use
Dependencies
at all. For injection, it is used standard initialisation injection:The implementation itself is hidden behind the factory function.
The
DependencyKey
implementation for the repository is moved into the data layer.The
DependencyKey
for UC is moved to the DI layer, where it is correctly initialised with the usage repository from the data layer.The featured client then depends on the UC. There is no need to mock data for preview. It is handled on the data layer.
The issue with this solution is that the presentation layer can access repositories from the data layer in this configuration. This means that in this solution, we are losing the main advantage of using separate packages—restricting the use of the data layer from the presentation layer on the compilation level.
Here is an example:
Pragmatic solution
Remove UCs from the domain layer and keep only the domain model shared among the presentation and data layers. This does not strictly follow clean architecture principles, but based on experience, most UCs look the same as in the example above. It is adding a boilerplate with almost no added value. Dependency clients can directly access repositories, as mentioned in the last example. I’m not sure if there are drawbacks to this simplification.
Beta Was this translation helpful? Give feedback.
All reactions