The Repository Pattern

  • avatar
  • 4.9K Views
  • 9 Likes
  • 8 mins read

In the ever-evolving world of software development, architects and developers strive to create scalable, maintainable, and robust applications. One of the key principles that aid in achieving these goals is the proper organization of data access and storage. The Repository Pattern is a fundamental architectural design pattern that provides a structured approach to handling data, making it an invaluable tool for developers. In this article, we will delve into what the Repository Pattern is, how it should be implemented, and provide some real-world examples of its application.

Understanding the Repository Pattern

At its core, the Repository Pattern is a design pattern that separates the logic that retrieves data from the underlying data storage (usually a database) from the rest of the application. This separation fosters modularity, maintainability, and testability, making it easier to adapt to changes in data sources or business requirements. In a typical implementation, a repository acts as an intermediary between the data access layer and the business logic layer. It abstracts the data access details, providing a clean and consistent API for the rest of the application to interact with, shielding it from the complexities of querying databases or other data sources.

Implementing the Repository Pattern

To implement the Repository Pattern effectively, developers should adhere to a few key principles:

  1. Abstraction: The repository should define a clear, high-level interface for data access operations, such as Create, Read, Update, and Delete (CRUD). This abstraction helps in decoupling the business logic from the data storage specifics.

  2. Separation of concerns: Keep the repository focused solely on data access and retrieval. Business logic and data transformation should be handled in separate layers of the application, promoting cleaner and more maintainable code.

  3. Dependency injection: Use dependency injection to provide repositories to the components that need them. This ensures that repositories can be easily swapped or mocked for testing purposes.

The Repository Pattern in action

Let's look at an example to illustrate the practical use of the Repository Pattern. In a web application, user management is a common requirement. By implementing a UserRepository, you can abstract away the details of how user data is stored (whether it's in a SQL database, NoSQL database, or even a third-party authentication service) and provide a consistent interface for creating, retrieving, updating, and deleting user records.

interface UserRepository
{
public function create(User $user): void;

public function update(User $user): void;

/**
* @throws UserNotFoundException
*/
public function findById(Id $userId): User;

public function searchById(Id $userId): ?User;

public function searchByCriteria(UserSearchCriteria $criteria): array;

public function delete(User $user): void;
}

The established interface is commonly referred to as a contract.

Implementing the Criteria Pattern

Criteria pattern or filter pattern is a design pattern that enables developers to filter a set of objects using different criteria and chaining them in a decoupled way through logical operations. This pattern combines multiple criteria to obtain single one. Shared domain logic will contain abstract criteria implementation that each specific criteria should extend from. A sample of an abstract criteria can be defined in the following way:

abstract class Criteria
{
private ?CriteriaPagination $pagination;
private ?CriteriaSort $sort;

protected function __construct(
?CriteriaPagination $pagination = null,
?CriteriaSort $sort = null
) {
$this->pagination = $pagination;
$this->sort = $sort;
}

public function paginateBy(CriteriaPagination $pagination): static
{
$this->pagination = $pagination;

return $this;
}

public function sortBy(CriteriaSort $sort): static
{
$this->sort = $sort;

return $this;
}

public function pagination(): ?CriteriaPagination
{
return $this->pagination;
}

public function sort(): ?CriteriaSort
{
return $this->sort;
}
}

It contains information related to pagination, such as elements per page and the offset:

final class CriteriaPagination
{
private Limit $limit;
private Offset $offset;

private function __construct(Limit $limit, Offset $offset)
{
$this->limit = $limit;
$this->offset = $offset;
}

public static function create(int $limit, ?int $offset = null): self
{
return new self(
Limit::fromInteger(max($limit, 0)),
Offset::fromInteger(max(($offset ?? 0), 0)),
);
}

public function limit(): Limit
{
return $this->limit;
}

public function offset(): Offset
{
return $this->offset;
}
}

Additionally, it handles the sorting, which involves specifying the field and the desired sorting direction:

final class CriteriaSort
{
private CriteriaSortField $field;
private CriteriaSortDirection $direction;

private function __construct(
CriteriaSortField $field,
CriteriaSortDirection $direction
) {
$this->field = $field;
$this->direction = $direction;
}

public static function create(string $field, CriteriaSortDirection $direction): self
{
return new self(
CriteriaSortField::fromString($field),
$direction,
);
}

public function field(): CriteriaSortField
{
return $this->field;
}

public function direction(): CriteriaSortDirection
{
return $this->direction;
}
}

The specific criteria, in turn, will contain the exact fields to filter. Within the defined users repository contract, the search method uses a UserSearchCriteria, which serves as the basis for constructing the necessary query to obtain user results:

final class UserSearchCriteria extends Criteria
{
public const PER_PAGE = 10;

private ?string $email = null;
private ?string $name = null;

public static function create(
string $email = null,
string $name = null
): UserSearchCriteria {
$criteria = new self(CriteriaPagination::create(self::PER_PAGE));

if (!empty($email)) {
$criteria->email = $email;
}

if (!empty($name)) {
$criteria->name = $name;
}

return $criteria;
}

public function email(): ?string
{
return $this->email;
}

public function name(): ?string
{
return $this->name;
}
}

The criteria object is passed to the repository and applied when building the query to retrieve data.

Conclusion

Architectural patterns like the Repository Pattern offer a guiding light for developers seeking to build scalable and maintainable applications. By abstracting data access, adhering to principles of separation of concerns, and utilizing dependency injection, the Repository Pattern empowers developers to create modular, flexible, and testable codebases. Whether you're working on a small project or a large-scale enterprise application, understanding and applying the Repository Pattern can be a valuable asset in your toolkit.

Credits

Official GitHub: https://github.com/hibit-dev/criteria

 Join Our Monthly Newsletter

Get the latest news and popular articles to your inbox every month

We never send SPAM nor unsolicited emails

0 Comments

Leave a Reply

Your email address will not be published.

Replying to the message: View original

Hey visitor! Unlock access to featured articles, remove ads and much more - it's free.