Domain Layer: Entities, Repositories, Use Cases

Let's map business rules into Domain Layer, define Entities, Repositories and UseCases and build application around it.

Domain Layer: Entities, Repositories, Use Cases

We have talked in previous article about domain layer, what it is and what its purpose is. Now, let's continue building Browser App and apply what we have learned so far.

Table of contents:

  1. Entities
  2. Repositories
  3. Use Cases
  4. Conclusion
  5. What is next?

UPD: The purpose of creation Domain Layer is to avoid dependency on backend and implement in separation. The responses I provide here may not be whole responses or whole structure, they are just for you to imagine what to features to expect from our application.

Entities

Let's recall what we have outlined in previous article:

We have basic search and searches with categories, but overall they are all search items with similar fields and unique fields based on category. That follows the idea that we create SearchEntity for basic search and each category will extend this entity and add their own fields, let's see in code, now, in more detail. Responses for "apple inc" basic search text in serper.dev.

{
    "organic": [
        {
            "title": "Apple",
            "link": "https://www.apple.com/",
            "snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
            "position": 1
        },
        {
            "title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com",
            "link": "https://www.bloomberg.com/quote/AAPL:US",
            "snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.",
            "position": 2
        },
        ...
    ],
}
class SearchEntity with EquatableMixin {
    const SearchEntity({
      required this.title,
      required this.link,
      required this.snippet,
      required this.position,
    });

    final String title;
    final String link;
    final String snippet;
    final int position;

    @override
    List<Object?> get props => [
          title,
          link,
          snippet,
          position,
        ];
}

Now, the same search for videos category.

{
    "videos": [
        {
            "title": "๐Ÿ”ฅ๐Ÿ˜ง APPLE Inc. The TRUTH ๐Ÿ˜จ๐Ÿ”ฅ",
            "link": "https://www.youtube.com/watch?v=P7slEUBcP0s",
            "snippet": "IF YOU LIKE THESE VIDEOS, YOU CAN MAKE A SMALL DONATION VIA PAYPAL or BITCOIN PAYPAL LINK: ...",
            "imageUrl": "https://i.ytimg.com/vi/P7slEUBcP0s/mqdefault.jpg?sqp=-oaymwEFCJQBEFM&rs=AMzJL3nb_ePHVd-QQzaoaRAWEkKdlEvDFw",
            "duration": "27:57",
            "source": "YouTube",
            "channel": "Theoria Apophasis",
            "date": "Feb 17, 2024",
            "position": 1
        },
        {
            "title": "Apple",
            "link": "https://www.youtube.com/apple",
            "snippet": "Apple Watch | Dear Apple | Apple. Apple. 1.5M views. 2 years ago. CC ยท iPhone ... Apple TV+ subscription: apple.co/_Apple-TV-Plus ยท 1:01. Eva the Owlet โ€” Season ...",
            "position": 2
        },
        ...
    ],
}
class VideoSearchEntity extends SearchEntity {
    const VideoSearchEntity({
        required super.title,
        required super.link,
        required super.snippet,
        required super.position,
        this.duration,
        this.source,
        this.channel,
        this.date,
    });

    final Duration? duration;
    final String? source;
    final String? channel;
    final DateTime? date;
  
    @override
    List<Object?> get props => [
          ...super.props,
          duration,
          source,
          channel,
          date,
        ];
}

From equatable package documentation:

Equatable overrides == and hashCode for you so you don't have to waste your time writing lots of boilerplate code. With Equatable there is no code generation needed and we can focus more on writing amazing applications and less on mundane tasks.

At this point, our dependency diagram is simple:

We have defined our entities in full, it is time to define what functions we expect in the applicaton.

Repositories

In previous articles, we talked about Repository Pattern. So adhering to those principles, we introduce our Repository Interface.

abstract interface class SearchRepository {
  Future<List<SearchEntity>> searchRemote(String searchText);

  Future<List<SearchEntity>> searchLocal(String searchText);

  Future<void> saveLocal(List<SearchEntity> items);
}

At this point, by looking repository interface, it is clear that we have search method to bring certain items when done searching. For now, we do not want to know how items are fetched from remote source or saved in some sort of cache, we focus only on what we want and that's it.

From the repository, what we want are:

  • search from remote source
  • search from local storage
  • save fetched items

To note, since SearchEntity is base class for all other types, this repository interface can be used to fetch items for other categories as well, but there maybe some type-casting issues, or we can introduce similar repositories for other categories as well.

When a single repository interface is used, there will be different implementations of this repository interface. We will focus on implementations when we talk about "data layer in practice".

Now, let's update dependency diagram where SearchRepository depends on SearchEntity.

Use Cases

From previous articles, we defined that each use case represents a single, well-defined task that the system can perform.

In our case, let's define the following task: "Search items online when there is internet connection and save them locally, otherwise search from local storage".

The following FetchSearchItemsUseCase depends on SearchRepository and Connectivity, and provides a method to fetch items from remote source when there is internet connection and save the new items in the local storage. In the absence of internet connection, it retrieves items from local storage and returns to point of call.

class FetchSearchItemsUseCase {
    FetchSearchItemsUseCase(
        this._searchRepository,
        this._connectivity,
    );
  
    final SearchRepository _searchRepository;
    final Connectivity _connectivity;

    Future<List<SearchEntity>> search(String searchText) async {
        if (_connectivity.hasConnection) {
            try {
                final remoteItems = await _searchRepository.searchRemote(searchText);
                await _searchRepository.saveLocal(remoteItems);
                return remoteItems;
            } on Object catch (_) {
                return await _searchLocal(searchText);
            }
        } else {
                return await _searchLocal(searchText);
        }
    }

    Future<List<SearchEntity>> _searchLocal(String searchText) {
        return _searchRepository.searchLocal(searchText);
    }
}

This is UseCase for basic search functionality. In the same fashion, we can develop FetchImageSearchItemsUseCase with the same SearchRepository interface, but with different implementation which will be injected to fulfill the need.

class FetchImageSearchItemsUseCase {
    FetchSearchItemsUseCase(
        this._searchRepository,
        this._connectivity,
    );
  
    final SearchRepository _searchRepository;
    final Connectivity _connectivity;

    Future<List<ImageSearchEntity>> search(String searchText) async {
        if (_connectivity.hasConnection) {
            try {
                final remoteItems = await _searchRepository.searchRemote(searchText);
                await _searchRepository.saveLocal(remoteItems);
                return remoteItems;
            } on Object catch (_) {
                return await _searchLocal(searchText);
            }
        } else {
                return await _searchLocal(searchText);
        }
    }

    Future<List<SearchEntity>> _searchLocal(String searchText) {
        return _searchRepository.searchLocal(searchText);
    }
}

The final dependency diagram would update to this:

Of course, this is just an example for you to imagine how UseCases would look like and do the job and there should be some proper type-casting. I will upload the final version in Github and attach here soon.

Conclusion

In this article, we have talked about Domain Layer, started with introducing Entities; defined function signatures to use on them in Repository Interface; outlined possible tasks to perform in the picture of Use Cases.

At this point, we have no UI and data layer, but when any engineer opens up our domain folder and goes through the above files, he will have an overview of a bigger picture in mind that we are building an application with searching functionality divided by categories. So it means that, we have mapped Business Requirements into our Domain Layer, that's the purpose of the layer.

What's next?

In the next article, we will be talking about Data Layer in detail; how api call to remote source is done; outline possible solutions for local storage; implement interface repositories focusing on how it is done.

GitHub - khamidjon/browser_app
Contribute to khamidjon/browser_app development by creating an account on GitHub.