· Baptiste Parmantier · Tutorials · 3 min read
Découvrez l'architecture hexagonale
Développer un bot Mineral c'est bien, découvrez comment mettre en place une architecture hexagonale pour qu'il soit maintenable et évolutif.
Introduction
Dans cet exemple nous prendrons le cas d’usage d’un bot de ticketing.
Structure
├── root
│ ├── src
│ │ ├── infrastructure
│ │ │ ├── services
│ │ │ └── repositories
│ │ │
│ │ ├── domain
│ │ │ ├── models
│ │ │ ├── services
│ │ │ └── contracts
│ │ │
│ │ ├── presentation
│ │ │ ├── commons
│ │ │ └── ticketing
│ │ │
│ │ └── main.dart
│ │
│ ├── pubspec.yaml
│ └── .env
infrastructure: contient les implémentations vers différentes sources de données externes, nous pouvons y retrouver des bases de données, caches, services externes, etc.domain: contient les règles métiers de l’application, les modèles de données, les services métiers et les contrats.presentation: contient les points d’entré du module, dans notre cadre, nous pouvons envisager des handlers écoutant des évènements reçus depuis Discord
Modèles
Nous allons créer les models :
final class SubjectTicket {
final int uid;
final String title;
final String description;
SubjectTicket({
required this.uid,
required this.title,
required this.description,
});
}
final class Ticket {
final int id;
final String subject;
final String ownerId;
Ticket({
required this.id,
required this.subject,
required this.ownerId,
});
factory Ticket.fromMap(Map<String, dynamic> map) {
return Ticket(
id: map['id'],
subject: map['subject'],
ownerId: map['owner_id'],
);
}
}
Services
abstract interface class DatabaseContract {
Executor get execute;
Future<void> init();
}
final class Database implements DatabaseContract {
final Connection _connection;
Executor get execute => _connection.execute;
Future<void> init() async {
final conn = await Connection.open(Endpoint(
host: 'localhost',
database: 'postgres',
username: 'user',
password: 'pass',
));
}
}
abstract interface class TicketRepositoryContract {
Future<Ticket> create(payload: CreateTicketPayload);
Future<voi> delete(int id);
}
final class TicketService implements TicketRepositoryContract {
final DatabaseContract _database;
Future<Ticket> create(payload: CreateTicketPayload) async {
final result = await _database.execute(
Sql.named('INSERT INTO tickets (subject, owner_id) VALUES (@subject, @ownerId)'),
parameters: { 'subject': payload.subject, 'ownerId': payload.ownerId }
);
return Ticket.fromMap(result);
}
Future<void> delete(int id) {
await _database.execute(
Sql.named('DELETE FROM tickets WHERE id = @id'),
parameters: { 'id': id }
);
}
}
abstract interface class TicketServiceContract {
List<SubjectTicket> get subjects;
Future<void> createTicket({ required SubjectTicket subject, required Member member});
}
final class TicketService with InjectLogger implements TicketServiceContract {
@override
final List<SubjectTicket> subjects = [
SubjectTicket(
uid: 1,
title: 'Report member',
description: 'Report a member to the staff'),
SubjectTicket(
uid: 2,
title: 'Ask a question',
description: 'Ask a question to the staff')
];
final TicketRepositoryContract _ticketRepository;
TicketService(this._ticketRepository);
Future<TextChannel> createTicket({required SubjectTicket subject, required CommandContext ctx}) async {
try {
await _ticketRepository.create(CreateTicketPayload(
subject: subject.uid,
ownerId: member.id
));
return member.guild.channels.create('ticket-${member.id}');
} catch(err) {
logger.error('An error occurred while creating a ticket: $err');
}
}
}
Dans cette partie de l’application nous allons utiliser plusieurs sources d’information entrantes telles que :
events: les évènements reçus depuis Discordcommands: les commandes reçues depuis Discord
Nous commencerons par créer une commande nous permettant d’interagir avec les tickets :
final class MyCommand implements CommandDeclaration {
final TicketServiceContract _ticketService;
MyCommand(this._ticketService);
FutureOr<void> handle(CommandContext ctx, {required int subject}) async {
final targetSubject = _ticketService.subjects
.firstWhere((element) => element.uid == subject);
await _ticketService.createTicket(
subject: targetSubject,
member: member
);
ctx.reply('Selected value: $value');
}
@override
CommandDeclarationBuilder build() {
return CommandDeclarationBuilder()
..setName('foo')
..setDescription('This is a command description')
..setHandler(handle)
..addOption(
ChoiceOption.integer(
name: 'subject',
description: 'Select a subject',
required: true,
choices: _ticketService.subjects
.map((e) => Choice(e.title, e.uid,))
.toList()
));
}
}