Acceder à un objet instancié dans une autre classe
Bonjour,
Dans la continuité de mon projet sur le développement d'app iOS/macOS pour les NAS Synology, je continue de développer ma propre librairie swift pour gérer les API constructeur.
Je suis actuellement entrain de gérer le design de mes classes.
J'ai actuellement une class Synology, qui est initialisé avec l'adresse du NAS.
C'est via cette classe entre autre qu'on se connecte et se déconnecte du NAS.
// Synology.swift
class Synology {
public var nas_address: String
init(address: String) {
self.nas_address = address
}
public let DS = DownloadStation()
func makeAPIRequest(...) {
// c'est ici qu'on execute les requêtes vers le serveur
}
...
}
Je veux ensuite développer un fichier / une class par API. Dans mon exemple, la class DownloadStation (DownloadStation.swift). Dans cette class, on gère alors tout ce qui est lié à l'API DownloadStation (récupération des taches de téléchargement, etc...)
// DownloadStation.swift
class DownloadStation {
public var tasks: Tasks = Tasks()
private let syno = ... // ici j'ai beosin de récupérer l'instance initialisée...
func getTasks(completionHandler: @escaping (Tasks, Bool) -> Void) {
syno.makeAPIRequest(....)
...
}
...
}
Je souhaite au final pouvoir utiliser ma librairie ainsi :
let nas: Synology = Synology(address: "192.168.0.100")
nas.DS.getTasks()
Ma question est : Comment utiliser l'objet Synology alors initialisé directement dans la class DownloadStation ? (Evidemment, on pourrait utiliser une instance déclarée dans ViewController, mais encore une fois, je souhaite écrire ma propre librairie, j'ai donc besoin du plus d'abstraction possible)
Réponses
Je pense avoir résolu cette question en passant la méthode makeAPIRequest de func à class func et ainsi j'appelle directement Synology.makeAPIRequest dans la classe DownloadStation.
Votre avis ? Est-ce une bonne solution ?
Il me semble plus correct d'ajouter un constructeur à la classe DownloadStation, prenant en paramètre une instance de la classe Synology...
Ta solution fonctionne, mais du coup elle limite ta librairie à une seule et unique connexion vers 1 NAS. Si on veut l'utiliser par exemple pour se connecter simultanément à plusieurs NAS, c'est mort.
L'injection de dépendance se fait de deux manières: soit en passant la dépendance à l'init, soit en utilisant un setter. La deuxième solution est à éviter, parce qu'elle complique le code, mais parfois on n'a pas le choix (oui UIViewController, je parle de toi).
Une autre solution est de ne pas injecter la dépendance, mais de la passer lors de l'utilisation. Par exemple:
Tu as donc deux solution:
- passer l'instance de Synology à l'init de DownloadStation
- ou lui demander downloadOnSynology(synology)
Comme zoc, je partirais plutôt sur la première solution qui est plus simple, et me semble mieux correspondre au besoin. La deuxième solution s'impose si tu veux qu'il n'y ait qu'une seule DownloadStation qui gère les téléchargements sur tous les NAS.
Comme zoc, je dirais non. Je dirais même pas du tout, c'est pas logique, pas équilibré.
Il faut que résonnes comme s'il pouvait y avoir plusieurs NAS sur le réseau. Même si tu ne supportes jamais ce cas, cela te permettra de faire une meilleure conception.
D'autre part, je n'aime pas ton idée de créer une classe pour chaque API, mais cela peut se discuter (alors que le point précédent ne se discute même pas).
Il ne faut pas confondre l'ordre et le concept.
Si tu veux bien ranger ton code (ordre), tu peux créer des extensions à la classe Synology. Une extension par API, chacune son fichier swift et tout est bien ordonné.
Par contre, en terme de conception, il faut avoir une bonne raison pour sortir chaque API de la classe Synology.
Les bonnes raisons possibles :
-certaines API sont optionnelles (système de plugin ou système d'option en fonction des modèles).
-certaines API existent en plusieurs exemplaires.
-toutes les API partagent des traitements communs, ce qui en fait des déclinaisons d'une API de base.
-une API peut changer de synology (à priori, non, cela n'a pas de sens).
-les API correspondent chacune à une ressource avec un "début" et une "fin".
FKDev, ton idée d'extension m'a traversé l'esprit, mais je souhaitais vraiment voir les appels effectués de la sorte :
Synology.DS.getTasks(), Synology.FileStation.createNewFile() ...
C'est peut-être une "erreur", ou plutôt un choix bancal, mais je l'imaginais comme ça.
Après n'oublions pas que je suis en plein apprentissage et que je profite un peu de ce projet pour toucher un peu à tous ces principes. Je ne m'interdit pas de restructurer en utilisant des Struct/Extensions.
Je pense donc que je vais suivre le conseil de Dependency Injection. Par contre, je me confronte donc à mon problème initial:
Si je comprends bien, je construit la classe DownloadStation ainsi :
L'objet Synology étant construit grâce au paramètre address (-> l'adresse du NAS), comment puis-je faire pour instancier l'objet Synology au sein même du constructeur de l'objet DownloadStation ?
Egalement, étant donné que j'instance l'objet DownloadStation au sein même de l'objet Synology, est-ce que cela ne risque pas de créer une sorte de "boucle infinie" ??
Désolé si mes questions sont trop "naà¯ves" ...
Merci encore
C'est bien ça ?
Ma question sur la "infinite loop" court toujours cependant
Autre petite question. J'ai également créer une struct Task qui gère donc la structure d'une tâche de téléchargement. Cependant dans cette struct, j'ai quelques méthodes qui font appel à Synology.makeAPIRequest().
Comment faire donc pour appeler une instance de Synology dans la struct ?
Pour avoir de l'injection de dépendance, il faut que tes objets implémentent des protocoles et qu'ils soient mis en relation par un autre objet.
Dans ton cas, cela pourrait avoir un intérêt si tu avais à supporter plusieurs modèles de Synology ayant chacun des API et des versions d'API différentes.
C'est pas une boucle infinie car cela va se terminer très vite par une exception de type stack overflow. En gros tu vas remplir la mémoire alloué à la pile avec des objets de type Synology et DownloadStation.
Si tu dois passer l'objet Synology à chaque tâche de téléchargement, cela va devenir moche.
Il faut que tu détermines ce dont a besoin chaque objet et que tu lui passes le minimum.
Par exemple, si cela se joue seulement au niveau de l'URL de l'API, tu peux procéder ainsi.
L'objet Synology construit la base de l'URL : "http://<adresse ip>/".
L'objet DownloadStation ajoute la commande : "http://<adresse ip>/Download?file=".
La tâche ajoute l'URL du fichier à télécharger : "http://<adresse ip>/Download?file=<url to download>".
Si tu as besoin de plus tu peux utiliser une struct pour passer plusieurs paramètres.
Si tu as besoin de méthodes, tu peux utiliser un protocol plutôt que le type Synology :
Donc en gros, pour résumer, tu dois rendre tes objets les moins dépendants les uns des autres possible en réduisant la voilure sur les données d'entrée de tes objets. Pour cela tu as plusieurs outils dont les protocol de swift.
Pourquoi veux-tu absolument passer par Synology pour récupérer la DownloadStation ?
DownloadStation a besoin d'un Synology pour fonctionner, mais Synology n'a pas besoin d'une DownloadStation.
Ex.:
Comme ça, tu n'as plus de références circulaires.
Comme c'est la DownloadStation qui crée la Task, elle peut très bien lui passer un Synology.
J'ai l'impression qu'il n'est pas bien clair dans ton esprit quel est le rôle des différents objets Synology, DownloadStation et Task. Idéalement, tu devrais pouvoir décrire le rôle de chaque classe en une ligne sans utiliser "et/ou".
https://en.wikipedia.org/wiki/Single_responsibility_principle
Si tu comptais, par exemple, d'avoir les différents types de connections vers le Synology, tu devrais utiliser les protocoles.
Petite démonstration :
et pour l'utiliser :
On pourrait avoir plusieurs connections :
Merci pour vos réponses, vraiment
Par contre, ça commence un peu à me dépasser là J'avoue que je décroche et que je ne vois pas trop comment je peux remanier mon code actuel pour le calquer à vos propositions...
Dans mon esprit :
- Synology est initialisé avec l'adresse du NAS.
-> Cette classe gère l'authentification de l'utilisateur avec son compte login/password, ainsi que la déconnection. (nécessaire pour la prise de contrôle de toutes les API, DownloadStation, FileStation, SurveillanceStation, bref toutes)
-> Cette classe contient également une méthode makeAPIRequest, qui effectue les requêtes à l'API.
makeAPIRequest utilise un objet NetworkManager (URLSession)
- DownloadStation gère les méthodes du types getTasksLists, createTask, ...
et les autres API gère leurs propres méthodes (getFilesList pour FileStation, getCamerasList pour SurveillanceStation, etc.)
- Task est le modèle d'une tâche de téléchargement, créé à partir du JSON reçu par la méthode getTasksLists. Il inclus les variables de type id, status, path_destination, url, download_speed, etc...
ainsi que des méthodes propres au tâche individuelle du type, pause(), resume(), delete(), edit()...
Mes requêtes transitent toutes donc via la méthode makeAPIRequest.
Cette méthode est donc nécessaire dans Synology (pour l'autentification par exemple), dans DownloadStation (pour la récupération des tâches par exemple) ou encore dans la struct Task (pour mettre en pause la tâche par exemple).
J'ai encore besoin de bien intégrer l'idée de Protocol, encore un peu abstrait pour le moment, et j'ai donc du mal à cerner maintenant comme modéliser tout ça avec un Protocol...
Personne sur mon dernier message ?
Bon, en attendant, j'ai tenté depuis de tout réécrire et ainsi de practicer avec Swift, les protocols, les extensions etc...
Donc maintenant, mon code est -entre autre - constitué :
- d'une classe Synology initialisée avec l'adresse du NAS et qui possède une méthode getJSON() qui éxecute une requête passée en paramètre, requête de type SynologyRequest.
- d'un protocol SynologyRequest
- d'une struct Request conforme au protocol SynologyRequest, initialisé par le chemin de l'API, la méthode (GET/POST) et les paramètres de la requête.
- d'une extension Synology pour gérer l'authentification (authenticate, disconnect, ....) au NAS
- d'une classe DownloadStation initialisée en passant un objet de type Synology en paramètre. Ainsi, je peux appeler la méthode getJSON de la classe Synology depuis l'objet DownloadStation. Cette classe comprend des méthodes de récupération (getDownloadTasksList()) de tâche de téléchargement de type Tasks, et de création de tâche de téléchargement de type Task.
- d'une struct Tasks initialisée avec le résultat JSON de la requête DownloadStation.getDownloadTasksList. Un objet Tasks est en fait une Array de Task
- d'une struct Task qui gère la structure d'une tâche.
Maintenant, pour récupérer le titre, par exemple, de la première tâche de téléchargement :
J'aimerais avoir votre avis là dessus - n'hésitez pas également si vous avez besoin de voir du code de mes différents objets.
Ce que je souhaite maintenant, c'est inclure les méthodes liées au Task directement dans la struct Task. Les méthodes du type pause(), resume(), etc.
Au final, pour mettre en pause une tâche :
Par contre, ma question est toujours un peu la même: comment appeler la méthode getJSON() depuis l'objet Task lui-même ?
Car à part passer l'objet Synology de DownloadStation à Tasks puis de Tasks à chaque Task, je ne pense pas que ce soit la bonne méthode, sans compter que cela me semble très lourd.
Je peux effectivement placer les méthodes liées au Task dans une extension de DownloadStation, mais cela reviendrait à appeler ceci pour mettre en pause une tâche. Je sens que c'est ce que vous allez me dire de faire cependant (mais je ne trouve pas ça naturel...)
Désolé pour le pavé, et merci encore.
En fait, je te conseille de te mettre au Test-Driven Development. C'est un excellent moyen de progresser, et le code répondra à une partie de tes questions: un code facilement testable est forcément simple.
Désolé Céroce, je pensais qu'il s'agissait d'un forum ici, et qu'on pouvait donc discuter autour d'un sujet ?
Je viens ici, discuter de ma façon de faire, ma façon d'apprendre, avec des personnes plus expertes que moi.
Je ne viens pas demander, par exemple, à ce qu'on rédige mon code à ma place?
Merci pour ta participation dans tous les cas, je vais aller également me renseigner sur ce qu'est le Test-Driven Development que tu sembles penser être une solution à ma problématique
Pour faire simple, le TDD (développement piloté par les tests) consiste à écrire tes tests unitaires (je ne sais pas si tu sais de quoi il retourne dans Xcode) avant d'écrire les méthodes et fonctions de ton application. Le but est de valider que ton code fonctionne et qu'il remplit le contrat que tu lui auras fixé.
1 - Tu écris tous les tests unitaires qui vont te garantir que ta fonction remplit bien son contrat
2 - Tu écris ton code sans forcément chercher à ce que ce dernier soit très beau
3 - Tu exécutes tes tests, si ils sont valides tu passes à l'étape suivante, sinon tu corriges jusqu'à que tout soit bon.
4 - Tu optimises, tu essayes de rendre ton code plus beau tout en t'assurant que tes tests sont toujours valides
Bien sûr que nous sommes ouverts à la discussion, mais il y a une limite "fonctionnelle" à l'aide que nous pouvons apporter sur un forum.
On peut facilement discuter ici de points de détails, ou de bonnes pratiques, mais je me vois mal donner des conseils quant à l'architecture de ton logiciel, sachant que je ne connais la problématique que de façon superficielle, que je n'y ai pas réfléchi, que je ne connais pas les contraintes, et qu'aux final, il existe plusieurs bonnes solutions.
Peut-être mon message est mal passé, mais le fond de ma pensée n'est pas " on s'en fout, démerde toi ", mais " tu es le plus à même à trouver les réponses ".