Acceder à  un objet instancié dans une autre classe

iosciosc Membre
novembre 2016 modifié dans API UIKit #1

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.

  • CéroceCéroce Membre, Modérateur
    novembre 2016 modifié #4
    Le terme que tu cherches est "injection de dépendance" (dependency injection). Une dépendance est un objet dont a besoin un autre objet pour faire son travail.
    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:
     
    class Dog {
    func playWith(ball: Ball) -> Void {

    }
    }
    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.
  • FKDEVFKDEV Membre
    novembre 2016 modifié #5

    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 ?


    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".
  • iosciosc Membre
    novembre 2016 modifié #6
    Merci beaucoup à  tous pour vos réponses, très intéressantes.

    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 :
     
    class DownloadStation {

        init() {
            let synology: Synology = Synology(address: ...)
        }

    }
     
    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
  • iosciosc Membre
    novembre 2016 modifié #7
    Bon j'ai finalement trouvé en essayant, soit :
     
    class DownloadStation {

        let synology: Synology

        init(dependency: Synology) {
            self.synology = dependency
        }
        ...
    }
     
    class Synology {

        var DS: DownloadStation!

         init(address: String) {
             ...
             self.DS = DownloadStation(dependency: self)
        }
        ...
    }
     
    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 ?
  • class DownloadStation {

        let synology: Synology

        init(dependency: Synology) {
            self.synology = dependency
        }
        ...
    }

    C'est ok. Mais on ne peut pas appeler cela de l'injection de dépendance, c'est un cas classique de composition. Un objet qui appartient à  un autre objet. Dans ton cas je pense que c'est suffisant.

    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.

    Ma question sur la "infinite loop" court toujours cependant ;)


    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.

    Comment faire donc pour appeler une instance de Synology dans la struct ?


    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 :


    init(dependency: URLProvider) {
    ...
    }

    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.
  • CéroceCéroce Membre, Modérateur
    Non.

    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.:

    let synology = Synology(address: "127.0.0.1")
    let station = DownloadStation(synology)
    let task = station.beginDownloadTask(filepath: "Toto.txt")
    Comme ça, tu n'as plus de références circulaires.
  • CéroceCéroce Membre, Modérateur

    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 ?


    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
  • Joanna CarterJoanna Carter Membre, Modérateur

    Si tu comptais, par exemple, d'avoir les différents types de connections vers le Synology, tu devrais utiliser les protocoles.


     


    Petite démonstration :



    protocol Connection
    {
    var address: String { get }

    init(address: String)

    func fetchData()

    func sendData()
    }


    struct FTPConnection : Connection
    {
    let address: String

    init(address: String)
    {
    self.address = String("FTP://\(address)")
    }

    func fetchData()
    {
    print("fetchData avec \(address)")
    }

    func sendData()
    {
    print("sendData avec \(address)")
    }
    }


    struct HTTPConnection : Connection
    {
    let address: String

    init(address: String)
    {
    self.address = String("HTTP://\(address)")
    }

    func fetchData()
    {
    print("fetchData avec \(address)")
    }

    func sendData()
    {
    print("sendData avec \(address)")
    }
    }


    struct Synology<ConnectionType : Connection>
    {
    private var connection: ConnectionType

    init(_ connection: ConnectionType)
    {
    self.connection = connection
    }

    func fetchData()
    {
    self.connection.fetchData()
    }

    func sendData()
    {
    self.connection.sendData()
    }
    }

    et pour l'utiliser :



    let connection = FTPConnection(address: "123.456.789.0")

    let ftpSynology = Synology(connection)

    ftpSynology.fetchData()

    ...
  • Joanna CarterJoanna Carter Membre, Modérateur
    novembre 2016 modifié #12

    On pourrait avoir plusieurs connections :



    enum SynologyError: Error
    {
    case fetchFailed
    case sendFailed
    }


    struct Synology
    {
    private var connections = [String : Connection]()

    init(_ connection: Connection, forIdentifier identifier: String)
    {
    self.add(connection: connection, forIdentifier: identifier)
    }

    mutating func add(connection: Connection, forIdentifier identifier: String)
    {
    self.connections[identifier] = connection
    }

    func fetchData(connectionIdentifier identifier: String) throws
    {
    guard let connection = self.connections[identifier] else
    {
    throw SynologyError.fetchFailed
    }

    connection.fetchData()
    }

    func sendData(connectionIdentifier identifier: String) throws
    {
    guard let connection = self.connections[identifier] else
    {
    throw SynologyError.sendFailed
    }

    connection.sendData()
    }
    }


    let ftpConnection = FTPConnection(address: "123.456.789.0")

    var synology = Synology(ftpConnection, forIdentifier:"FTP")

    let httpConnection = HTTPConnection(address: "987.654.321.0")

    synology.add(connection: httpConnection, forIdentifier:"HTTP")

    do
    {
    try synology.fetchData(connectionIdentifier: "FTP")
    }
    catch
    {
    print("Fetch failed")
    }

    do
    {
    try synology.sendData(connectionIdentifier: "HTTP")
    }
    catch
    {
    print("Send failed")
    }

  • iosciosc Membre
    novembre 2016 modifié #13
     

    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 :

     



    let nas: Synology = Synology(address: "123.456.789.0")
    nas.authenticate(credential: ...) // je passe sur la méthode d'authentification
    let downloadStation: DownloadStation = DownloadStation(device: nas)

    downloadStation.getDownloadTasksList(completion: { tasks in 
        print(tasks.task[0].title)
    })

    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 :

     



    tasks.task[0].pause()

    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...) :)

     



    downloadStation.pause(task: taskID)

      

    Désolé pour le pavé, et merci encore.


     

     

  • CéroceCéroce Membre, Modérateur
    Si tu veux, nous n'allons pas décortiquer ton code, surtout que nous ne connaissons pas le système.
    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 ;)

  • JérémyJérémy Membre
    novembre 2016 modifié #17


     


    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


  • CéroceCéroce Membre, Modérateur

    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.

    C'est pas le problème. Je t'explique juste pourquoi tu n'as pas eu de réponse.
    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 ".
Connectez-vous ou Inscrivez-vous pour répondre.