
Bonjour à tous! Je m'appelle Ilya, je suis développeur iOS sur Tinkoff.ru. Dans cet article, je veux parler de la façon de réduire la duplication de code dans la couche de présentation à l'aide de protocoles.
Quel est le problĂšme?
à mesure que le projet se développe, la quantité de duplication de code augmente. Cela ne devient pas immédiatement apparent et il devient difficile de corriger les erreurs du passé. Nous avons remarqué ce problÚme sur notre projet et l'avons résolu en utilisant une approche, appelons-le, conditionnellement, des traits.
Exemple de vie
L'approche peut ĂȘtre utilisĂ©e avec diffĂ©rentes solutions architecturales diffĂ©rentes, mais je la considĂ©rerai en utilisant VIPER comme exemple.
Considérez la méthode la plus courante dans le routeur - la méthode qui ferme l'écran:
func close() { self.transitionHandler.dismiss(animated: true, completion: nil) }
Il est présent dans de nombreux routeurs et il est préférable de l'écrire une seule fois.
L'héritage nous y aiderait, mais à l'avenir, lorsque nous aurons de plus en plus de classes avec des méthodes inutiles dans notre application, ou que nous ne serons pas en mesure de créer la classe dont nous avons besoin car les méthodes requises sont dans différentes classes de base, de grandes apparaßtront problÚmes.
En conséquence, le projet se développera en de nombreuses classes de base et classes descendantes avec des méthodes superflues. L'hérédité ne nous aidera pas.
Quoi de mieux que l'héritage? Bien sûr, la composition.
Vous pouvez créer une classe distincte pour la méthode qui ferme l'écran et l'ajouter à chaque routeur dans lequel elle est nécessaire:
struct CloseRouter { let transitionHandler: UIViewController func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
Nous devons encore dĂ©clarer cette mĂ©thode dans le protocole d'entrĂ©e du routeur et l'implĂ©menter dans le routeur lui-mĂȘme:
protocol SomeRouterInput { func close() } class SomeRouter: SomeRouterInput { var transitionHandler: UIViewController! lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }() func close() { self.closeRouter.close() } }
Il s'est avéré trop de code qui fait simplement office de proxy pour la méthode close.
Le bon programmeur
paresseux n'appréciera pas.
Solution de protocole
Les protocoles viennent à la rescousse. Il s'agit d'un outil assez puissant qui vous permet d'implémenter la composition et peut contenir des méthodes d'implémentation en extension. Nous pouvons donc créer un protocole contenant la méthode close et l'implémenter en extension.
Voici Ă quoi cela ressemblera:
protocol CloseRouterTrait { var transitionHandler: UIViewController! { get } func close() } extension CloseRouterTrait { func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
La question est, pourquoi le mot trait apparaĂźt-il dans le nom du protocole? C'est simple - vous pouvez spĂ©cifier que ce protocole implĂ©mente ses mĂ©thodes en extension et doit ĂȘtre utilisĂ© en tant que mĂ©lange avec un autre type pour Ă©tendre ses fonctionnalitĂ©s.
Voyons maintenant Ă quoi ressemblera l'utilisation d'un tel protocole:
class SomeRouter: CloseRouterTrait { var transitionHandler: UIViewController! }
Oui, câest tout. Ăa a l'air gĂ©nial :). Nous avons obtenu la composition en ajoutant le protocole Ă la classe du routeur, n'avons pas Ă©crit une seule ligne supplĂ©mentaire et avons eu la possibilitĂ© de rĂ©utiliser le code.
Qu'est-ce qui est inhabituel dans cette approche?
Vous avez peut-ĂȘtre dĂ©jĂ posĂ© cette question. L'utilisation de protocoles comme trait est assez courante. La principale diffĂ©rence est d'utiliser cette approche comme solution architecturale au sein de la couche de prĂ©sentation. Comme toute solution architecturale, il devrait y avoir ses propres rĂšgles et recommandations.
Voici ma liste:
- Les traits ne doivent pas stocker et changer d'état. Ils ne peuvent avoir que des dépendances sous forme de services, etc., qui sont des propriétés get-only.
- Les traits ne devraient pas avoir de méthodes qui ne sont pas implémentées en extension, car cela viole leur concept
- Les noms des mĂ©thodes dans trait doivent reflĂ©ter explicitement ce qu'ils font, sans ĂȘtre liĂ©s au nom du protocole. Cela aidera Ă Ă©viter les collisions de noms et Ă rendre le code plus clair.
De VIPER Ă MVP
Si vous passez complĂštement Ă l'utilisation de cette approche avec des protocoles, les classes routeur et interacteur ressembleront Ă ceci:
class SomeRouter: CloseRouterTrait, OtherRouterTrait { var transitionHandler: UIViewController! } class SomeInteractor: SomeInteractorTrait { var someService: SomeServiceInput! }
Cela ne s'applique pas à toutes les classes; dans la plupart des cas, le projet aura simplement des routeurs et des interacteurs vides. Dans ce cas, vous pouvez perturber la structure du module VIPER et passer en douceur à MVP en ajoutant des protocoles d'impuretés au présentateur.
Quelque chose comme ça:
class SomePresenter: CloseRouterTrait, OtherRouterTrait, SomeInteractorTrait, OtherInteractorTrait { var transitionHandler: UIViewController! var someService: SomeSericeInput! }
Oui, la possibilité d'implémenter un routeur et un interacteur en tant que dépendances est perdue, mais dans certains cas, c'est le cas.
Le seul inconvĂ©nient est transitionHandler = UIViewController. Et selon les rĂšgles de VIPER Presenter, rien ne doit ĂȘtre connu sur la couche View et comment elle est implĂ©mentĂ©e Ă l'aide de quelles technologies. Ceci est rĂ©solu dans ce cas simplement - les mĂ©thodes de transition de UIViewController sont "fermĂ©es" par le protocole, par exemple, TransitionHandler. Ainsi, le prĂ©sentateur interagira avec l'abstraction.
Changer le comportement des traits
Voyons comment vous pouvez changer le comportement dans de tels protocoles. Ce sera un analogue de la substitution de certaines parties du module, par exemple, pour des tests ou un talon temporaire.
Par exemple, prenons un simple interacteur avec une mĂ©thode qui effectue une requĂȘte rĂ©seau:
protocol SomeInteractorTrait { var someService: SomeServiceInput! { get } func performRequest(completion: @escaping (Response) -> Void) } extension SomeInteractorTrait { func performRequest(completion: @escaping (Response) -> Void) { someService.performRequest(completion) } }
C'est un code abstrait, par exemple. Supposons que nous n'ayons pas besoin d'envoyer une demande, mais simplement de renvoyer une sorte de talon. Ici, nous allons à l'astuce - créer un protocole vide appelé Mock et procédez comme suit:
protocol Mock {} extension SomeInteractorTrait where Self: Mock { func performRequest(completion: @escaping (Response) -> Void) { completion(MockResponse()) } }
Ici, l'implémentation de la méthode performRequest a été modifiée pour les types qui implémentent le protocole Mock. Vous devez maintenant implémenter le protocole Mock pour la classe qui implémentera SomeInteractor:
class SomePresenter: SomeInteractorTrait, Mock { // Implementation }
Pour la classe SomePresenter, l'implĂ©mentation de la mĂ©thode performRequest sera appelĂ©e, situĂ©e en extension, oĂč Self satisfait le protocole Mock. Il vaut la peine de supprimer le protocole Mock et l'implĂ©mentation de la mĂ©thode performRequest sera prise de l'extension habituelle Ă SomeInteractor.
Si vous ne l'utilisez que pour des tests, il est préférable de placer tout le code associé à la substitution de l'implémentation dans la cible de test.
Pour résumer
En conclusion, il convient de noter les avantages et les inconvĂ©nients de cette approche et dans quels cas, Ă mon avis, elle vaut la peine d'ĂȘtre utilisĂ©e.
Commençons par les inconvénients:
- Si vous vous débarrassez du routeur et de l'interacteur, comme indiqué dans l'exemple, la possibilité d'implémenter ces dépendances est perdue.
- Un autre inconvénient est le nombre fortement croissant de protocoles.
- Parfois, le code peut ne pas sembler aussi clair que l'utilisation d'approches conventionnelles.
Les aspects positifs de cette approche sont les suivants:
- Plus important et évident, la duplication est considérablement réduite.
- La liaison statique est appliquĂ©e aux mĂ©thodes de protocole. Cela signifie que la dĂ©termination de la mise en Ćuvre de la mĂ©thode se fera au stade de la compilation. Par consĂ©quent, pendant l'exĂ©cution du programme, aucun temps supplĂ©mentaire ne sera consacrĂ© Ă la recherche d'une implĂ©mentation (bien que ce temps ne soit pas particuliĂšrement important).
- Du fait que les protocoles sont de petites «briques», toute composition peut ĂȘtre facilement composĂ©e Ă partir d'eux. De plus en karma pour une flexibilitĂ© d'utilisation.
- Facilité de refactoring, aucun commentaire ici.
- Vous pouvez commencer à utiliser cette approche à n'importe quelle étape du projet, car elle n'affecte pas l'intégralité du projet.
Considérer cette décision comme bonne ou non est une affaire privée pour tout le monde. Notre expérience avec cette approche a été positive et a résolu des problÚmes.
Câest tout!