Composition Root: cum scrii software modular

  • 2 minute de citit
  • 10 exemple de cod
  • 1 imagine

Disponibil și în English

Composition root este un design pattern care te ajută să structurezi o aplicație software, implementând o clasă care construiește toate celelalte clase.

În acest exemplu vom analiza acest pattern în Python.

Iată graful de obiecte al claselor pe care urmează să le implementăm:

Am proiectat o aplicație de test pe care o vom folosi. Are trei componente: ConsoleInputListener, ConsolePrinter și RomanianTranslator, și o clasă de tip value object: Message.

Clasele sunt descrise după cum urmează:

  • Application: Composition root, leagă toate clasele între ele.
  • ConsoleInputListener: Componentă, citește șiruri de caractere de la intrarea standard.
  • ConsolePrinter: Componentă, afișează la ieșirea standard.
  • RomanianTranslator: Componentă, traduce cuvinte din engleză în română.
  • Message: Value object, încapsulează șirul de caractere al mesajului.

*

Programează pentru o interfață, nu pentru o implementare *

Înainte să implementez componenta Application, voi defini interfețele pentru ConsoleInputListener, ConsolePrinter și RomanianTranslator. Le voi numi InputListener, Printer și Translator, pentru simplitate.

Motivul pentru care definesc interfețele* este că vreau să pot schimba obiectele pe care le referențiază clasa Application. În Python variabilele nu mă constrâng la niciun tip, dar dacă voi implementa alte obiecte, vreau să am un șablon care mă ajută să fac mai puține greșeli.

Python nu are suport pentru interfețe, așa că voi folosi clase abstracte:

class Printer(metaclass=abc.ABCMeta): def print(self, message): raise NotImplementedError(“print is not implemented”)

class InputListener(metaclass=abc.ABCMeta): def get_input(self) -> str: raise NotImplementedError(“get_input is not implemented!”)

class Translator(metaclass=abc.ABCMeta): def translate(self, message: Message) -> Message: raise NotImplementedError(“translate must be implemented!”)

Fiecare clasă care extinde clasele mele abstracte trebuie să își implementeze metodele abstracte:

<pre class="wp-block-code">```class ConsolePrinter(Printer):    def __init__(self, prefix: str):        self._prefix = prefix    def print(self, message: Message):        print(self._prefix, message)class ConsoleInputListener(InputListener):    def __init__(self, prompt: str):        self._prompt = prompt    def get_input(self) -> str:        return input(self._prompt)class RomanianTranslator(Translator):    def translate(self, message: Message) -> Message:        words_map = {"hello": "salut"}        message_words = str(message).split(" ")        for index, word in enumerate(message_words):            if word.lower() in words_map.keys():                message_words[index] = words_map[word]        return Message(" ".join(message_words))
Clasa Message, pentru completitudine, păstrează doar un șir de caractere.
class Message:    def __init__(self, message):        self._message = message    def __str__(self):        return self._message
Și, în final, clasa Application va lega toate componentele între ele și le va instanția:
from input_listener import InputListener, ConsoleInputListenerfrom message import Messagefrom printer import Printer, ConsolePrinterfrom translator import Translator, RomanianTranslatorclass Application:    def __init__(self):        self._printer: Printer = ConsolePrinter(">")        self._translator: Translator = RomanianTranslator()        self._input_listener: InputListener = ConsoleInputListener("< ")    def start(self):        print("starting application.")        while True:            user_in = Message(self._input_listener.get_input())            if str(user_in) == "exit":                exit(0)            self._printer.print(self._translator.translate(user_in))
Metoda main va porni doar Application:
from application import Applicationdef main():    app = Application()    app.start()if __name__ == '__main__':    main()
Rularea aplicației ar afișa:
 starting application.
 < hello Denis!
 > salut Denis! 
```


- - - - - -


Acum, majoritatea aplicațiilor reale nu sunt atât de simple. Motivul pentru care am parcurs tot acest cod ca să implementăm un simplu *hello world* este următorul: imaginează-ți că ai 10 traducători: engleză, franceză, germană… și două componente de afișare: Console și File.


Ai putea modifica codul aplicației ca să primească doi parametri: translator și printer, și să folosești argumentele pentru a instanția traducătorul și printerul corecte, fără să fie nevoie să schimbi celelalte clase. Poți adăuga oricâte printere, traducători și input listeners dorești. Acesta este un avantaj uriaș.


Dacă ai fi pus tot codul inline într-o singură clasă, adăugarea de mai multe traduceri și de mai multe opțiuni de afișare ar fi fost foarte dureroasă și frustrantă.


Sper că articolul meu ți-a oferit o înțelegere a pattern-ului Composition Root. Dacă am greșit cu ceva, te rog să mă corectezi. 🙂


Codul complet se află pe [Github-ul meu](https://github.com/dnutiu/NucuLabs-code/tree/master/python-component-root-design-pattern).


Mulțumesc că ai citit!