CLI-Anwendung + Abhängigkeitsinjektor - Leitfaden zur Abhängigkeitsinjektion + FAQ

Hallo,



ich bin der Schöpfer von Dependency Injector . Dies ist ein Abhängigkeitsinjektionsframework für Python.



Dies ist die endgültige Anleitung zum Erstellen von Anwendungen mit dem Abhängigkeitsinjektor. In früheren Tutorials wurde beschrieben, wie Sie eine Webanwendung mit Flask , eine REST-API mit Aiohttp und die Überwachung eines Daemons mit Asyncio mithilfe der Abhängigkeitsinjektion erstellen .



Heute möchte ich zeigen, wie Sie eine Konsolenanwendung (CLI) erstellen können.



Zusätzlich habe ich Antworten auf häufig gestellte Fragen vorbereitet und werde deren Nachtrag veröffentlichen.



Das Handbuch besteht aus folgenden Teilen:



  1. Was werden wir bauen?
  2. Umwelt vorbereiten
  3. Projektstruktur
  4. Abhängigkeiten installieren
  5. Vorrichtungen
  6. Container
  7. Arbeiten mit CSV
  8. Arbeiten mit SQLite
  9. Anbieterauswahl
  10. Tests
  11. Fazit
  12. PS: Fragen und Antworten


Das abgeschlossene Projekt finden Sie auf Github .



Um zu beginnen, mĂĽssen Sie haben:



  • Python 3.5+
  • Virtuelle Umgebung


Und es ist wünschenswert, ein allgemeines Verständnis des Prinzips der Abhängigkeitsinjektion zu haben.



Was werden wir bauen?



Wir werden eine CLI-Anwendung (Konsole) erstellen, die nach Filmen sucht. Nennen wir es Movie Lister.



Wie funktioniert Movie Lister?



  • Wir haben eine Datenbank mit Filmen
  • Zu jedem Film sind folgende Informationen bekannt:

    • Name
    • Baujahr
    • Name des Direktors
  • Die Datenbank ist in zwei Formaten verteilt:

    • CSV-Datei
    • SQLite-Datenbank
  • Die Anwendung durchsucht die Datenbank anhand der folgenden Kriterien:

    • Name des Direktors
    • Baujahr
  • Andere Datenbankformate können in Zukunft hinzugefĂĽgt werden


Movie Lister ist eine Beispielanwendung, die in Martin Fowlers Artikel über Abhängigkeitsinjektion und Inversion der Kontrolle verwendet wird.



So sieht das Klassendiagramm der Movie Lister-Anwendung aus:





Die Verantwortlichkeiten zwischen den Klassen sind wie folgt verteilt:



  • MovieLister - verantwortlich fĂĽr die Suche
  • MovieFinder - verantwortlich fĂĽr das Extrahieren von Daten aus der Datenbank
  • Movie - Entitätsklasse "Film"


Umwelt vorbereiten



Beginnen wir mit der Vorbereitung der Umgebung.



Zunächst müssen wir einen Projektordner und eine virtuelle Umgebung erstellen:



mkdir movie-lister-tutorial
cd movie-lister-tutorial
python3 -m venv venv


Jetzt aktivieren wir die virtuelle Umgebung:



. venv/bin/activate


Die Umgebung ist bereit. Kommen wir nun zur Struktur des Projekts.



Projektstruktur



In diesem Abschnitt organisieren wir die Struktur des Projekts.



Lassen Sie uns die folgende Struktur im aktuellen Ordner erstellen. Lassen Sie alle Dateien vorerst leer.



Anfangsstruktur:



./
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   └── containers.py
├── venv/
├── config.yml
└── requirements.txt


Abhängigkeiten installieren



Es ist Zeit, die Abhängigkeiten zu installieren. Wir werden Pakete wie dieses verwenden:



  • dependency-injector - Rahmen fĂĽr die Abhängigkeitsinjektion
  • pyyaml - Bibliothek zum Parsen von YAML-Dateien, die zum Lesen der Konfiguration verwendet wird
  • pytest - Testrahmen
  • pytest-cov - Hilfsbibliothek zur Messung der Codeabdeckung durch Tests


FĂĽgen wir der Datei die folgenden Zeilen hinzu requirements.txt:



dependency-injector
pyyaml
pytest
pytest-cov


Und im Terminal ausfĂĽhren:



pip install -r requirements.txt


Die Installation der Abhängigkeiten ist abgeschlossen. Weiter zu den Spielpaarungen.



Vorrichtungen



In diesem Abschnitt werden wir Fixtures hinzufĂĽgen. Testdaten werden als Vorrichtungen bezeichnet.



Wir werden ein Skript erstellen, das Testdatenbanken erstellt.



FĂĽgen Sie data/dem Stammverzeichnis des Projekts ein Verzeichnis hinzu und fĂĽgen Sie eine Datei hinzu fixtures.py:



./
├── data/
│   └── fixtures.py
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   └── containers.py
├── venv/
├── config.yml
└── requirements.txt


Als nächstes bearbeiten fixtures.py:



"""Fixtures module."""

import csv
import sqlite3
import pathlib


SAMPLE_DATA = [
    ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
    ('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'),
    ('The Jungle Book', 2016, 'Jon Favreau'),
]

FILE = pathlib.Path(__file__)
DIR = FILE.parent
CSV_FILE = DIR / 'movies.csv'
SQLITE_FILE = DIR / 'movies.db'


def create_csv(movies_data, path):
    with open(path, 'w') as opened_file:
        writer = csv.writer(opened_file)
        for row in movies_data:
            writer.writerow(row)


def create_sqlite(movies_data, path):
    with sqlite3.connect(path) as db:
        db.execute(
            'CREATE TABLE IF NOT EXISTS movies '
            '(title text, year int, director text)'
        )
        db.execute('DELETE FROM movies')
        db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data)


def main():
    create_csv(SAMPLE_DATA, CSV_FILE)
    create_sqlite(SAMPLE_DATA, SQLITE_FILE)
    print('OK')


if __name__ == '__main__':
    main()


Lassen Sie uns nun im Terminal ausfĂĽhren:



python data/fixtures.py


Das Skript sollte OKbei Erfolg ausgegeben werden .



Wir ĂĽberprĂĽfen, ob die Dateien movies.csvund movies.dbim Verzeichnis erschienen sind data/:



./
├── data/
│   ├── fixtures.py
│   ├── movies.csv
│   └── movies.db
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   └── containers.py
├── venv/
├── config.yml
└── requirements.txt


Fixtures werden erstellt. Lass uns weitermachen.



Container



In diesem Abschnitt fĂĽgen wir den Hauptteil unserer Anwendung hinzu - den Container.



Mit dem Container können Sie die Struktur der Anwendung deklarativ beschreiben. Es enthält alle Anwendungskomponenten und ihre Abhängigkeiten. Alle Abhängigkeiten werden explizit angegeben. Anbieter werden verwendet, um dem Container Anwendungskomponenten hinzuzufügen. Anbieter steuern die Lebensdauer von Komponenten. Beim Erstellen eines Anbieters wird keine Komponente erstellt. Wir teilen dem Anbieter mit, wie das Objekt erstellt werden soll, und er wird es so schnell wie nötig erstellen. Wenn die Abhängigkeit eines Anbieters ein anderer Anbieter ist, wird sie entlang der Abhängigkeitskette aufgerufen und so weiter.



Lassen Sie uns bearbeiten containers.py:



"""Containers module."""

from dependency_injector import containers


class ApplicationContainer(containers.DeclarativeContainer):
    ...


Der Behälter ist noch leer. In den nächsten Abschnitten werden wir Anbieter hinzufügen.



FĂĽgen wir eine weitere Funktion hinzu main(). Ihre Aufgabe ist es, die Anwendung auszufĂĽhren. Im Moment wird sie nur einen Container erstellen.



Lassen Sie uns bearbeiten __main__.py:



"""Main module."""

from .containers import ApplicationContainer


def main():
    container = ApplicationContainer()


if __name__ == '__main__':
    main()


Der Container ist das erste Objekt in der Anwendung. Es wird verwendet, um alle anderen Objekte abzurufen.


Arbeiten mit CSV



Fügen wir nun alles hinzu, was wir für die Arbeit mit CSV-Dateien benötigen.



Wir brauchen:



  • Die Essenz Movie
  • Basisklasse MovieFinder
  • Seine Umsetzung CsvMovieFinder
  • Klasse MovieLister


Nachdem wir jede Komponente hinzugefĂĽgt haben, fĂĽgen wir sie dem Container hinzu.







Erstellen Sie eine Datei entities.pyin einem Paket movies:



./
├── data/
│   ├── fixtures.py
│   ├── movies.csv
│   └── movies.db
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   ├── containers.py
│   └── entities.py
├── venv/
├── config.yml
└── requirements.txt


und fĂĽgen Sie die folgenden Zeilen hinzu:



"""Movie entities module."""


class Movie:

    def __init__(self, title: str, year: int, director: str):
        self.title = str(title)
        self.year = int(year)
        self.director = str(director)

    def __repr__(self):
        return '{0}(title={1}, year={2}, director={3})'.format(
            self.__class__.__name__,
            repr(self.title),
            repr(self.year),
            repr(self.director),
        )


Jetzt müssen wir Moviedem Container eine Fabrik hinzufügen . Dafür benötigen wir ein Modul providersvon dependency_injector.



Lassen Sie uns bearbeiten containers.py:



"""Containers module."""

from dependency_injector import containers, providers

from . import entities

class ApplicationContainer(containers.DeclarativeContainer):

    movie = providers.Factory(entities.Movie)


Vergessen Sie nicht, die Auslassungspunkte ( ...) zu entfernen . Der Container hat bereits Anbieter und wird nicht mehr benötigt.


Fahren wir mit dem Erstellen fort finders.



Erstellen Sie eine Datei finders.pyin einem Paket movies:



./
├── data/
│   ├── fixtures.py
│   ├── movies.csv
│   └── movies.db
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   ├── containers.py
│   ├── entities.py
│   └── finders.py
├── venv/
├── config.yml
└── requirements.txt


und fĂĽgen Sie die folgenden Zeilen hinzu:



"""Movie finders module."""

import csv
from typing import Callable, List

from .entities import Movie


class MovieFinder:

    def __init__(self, movie_factory: Callable[..., Movie]) -> None:
        self._movie_factory = movie_factory

    def find_all(self) -> List[Movie]:
        raise NotImplementedError()


class CsvMovieFinder(MovieFinder):

    def __init__(
            self,
            movie_factory: Callable[..., Movie],
            path: str,
            delimiter: str,
    ) -> None:
        self._csv_file_path = path
        self._delimiter = delimiter
        super().__init__(movie_factory)

    def find_all(self) -> List[Movie]:
        with open(self._csv_file_path) as csv_file:
            csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
            return [self._movie_factory(*row) for row in csv_reader]


FĂĽgen wir CsvMovieFindernun den Container hinzu.



Lassen Sie uns bearbeiten containers.py:



"""Containers module."""

from dependency_injector import containers, providers

from . import finders, entities

class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    movie = providers.Factory(entities.Movie)

    csv_finder = providers.Singleton(
        finders.CsvMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.csv.path,
        delimiter=config.finder.csv.delimiter,
    )


Sie CsvMovieFindersind von der Fabrik abhängig Movie. CsvMovieFinderbenötigt eine Factory, da sie Objekte Moviebeim Lesen von Daten aus einer Datei erstellt. Um die Fabrik zu übergeben, verwenden wir das Attribut .provider. Dies wird als Providerdelegation bezeichnet. Wenn wir eine Fabrik geben movieals Abhängigkeit, wird es aufgerufen werden , wenn es csv_findererstellt wird CsvMovieFinderund ein Objekt wird als Injektion übergeben werden Movie. Die Verwendung des Attributs .providerals Injektion wird vom Anbieter selbst übergeben.



Es csv_finderhängt auch von mehreren Konfigurationsoptionen ab. Wir haben einen Anbieter hinzugefügt onfiguration, um diese Abhängigkeiten zu übergeben.



Wir haben die Konfigurationsparameter verwendet, bevor wir ihre Werte eingestellt haben. Dies ist das Prinzip, nach dem der Anbieter arbeitet Configuration.



Zuerst verwenden wir, dann setzen wir die Werte.



FĂĽgen wir nun die Konfigurationswerte hinzu.



Lassen Sie uns bearbeiten config.yml:



finder:

  csv:
    path: "data/movies.csv"
    delimiter: ","


Die Werte werden auf die Konfigurationsdatei gesetzt. Lassen Sie uns die Funktion aktualisieren main(), um ihren Standort anzugeben.



Lassen Sie uns bearbeiten __main__.py:



"""Main module."""

from .containers import ApplicationContainer


def main():
    container = ApplicationContainer()

    container.config.from_yaml('config.yml')


if __name__ == '__main__':
    main()


Lass uns gehen zu listers.



Erstellen Sie eine Datei listers.pyin einem Paket movies:



./
├── data/
│   ├── fixtures.py
│   ├── movies.csv
│   └── movies.db
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   ├── containers.py
│   ├── entities.py
│   ├── finders.py
│   └── listers.py
├── venv/
├── config.yml
└── requirements.txt


und fĂĽgen Sie die folgenden Zeilen hinzu:



"""Movie listers module."""

from .finders import MovieFinder


class MovieLister:

    def __init__(self, movie_finder: MovieFinder):
        self._movie_finder = movie_finder

    def movies_directed_by(self, director):
        return [
            movie for movie in self._movie_finder.find_all()
            if movie.director == director
        ]

    def movies_released_in(self, year):
        return [
            movie for movie in self._movie_finder.find_all()
            if movie.year == year
        ]


Wir aktualisieren containers.py:



"""Containers module."""

from dependency_injector import containers, providers

from . import finders, listers, entities

class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    movie = providers.Factory(entities.Movie)

    csv_finder = providers.Singleton(
        finders.CsvMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.csv.path,
        delimiter=config.finder.csv.delimiter,
    )

    lister = providers.Factory(
        listers.MovieLister,
        movie_finder=csv_finder,
    )


Alle Komponenten werden erstellt und dem Container hinzugefĂĽgt.



SchlieĂźlich aktualisieren wir die Funktion main().



Lassen Sie uns bearbeiten __main__.py:



"""Main module."""

from .containers import ApplicationContainer


def main():
    container = ApplicationContainer()

    container.config.from_yaml('config.yml')

    lister = container.lister()

    print(
        'Francis Lawrence movies:',
        lister.movies_directed_by('Francis Lawrence'),
    )
    print(
        '2016 movies:',
        lister.movies_released_in(2016),
    )


if __name__ == '__main__':
    main()


Alles ist fertig. Starten wir nun die Anwendung.



Lassen Sie uns im Terminal ausfĂĽhren:



python -m movies


Du wirst sehen:



Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]


Unsere Anwendung arbeitet mit einer Datenbank von Filmen in csv. Wir müssen auch Formatunterstützung hinzufügen sqlite. Wir werden uns im nächsten Abschnitt damit befassen.



Arbeiten mit SQLite



In diesem Abschnitt fĂĽgen wir einen weiteren Typ hinzu MovieFinder- SqliteMovieFinder.



Lassen Sie uns bearbeiten finders.py:



"""Movie finders module."""

import csv
import sqlite3
from typing import Callable, List

from .entities import Movie


class MovieFinder:

    def __init__(self, movie_factory: Callable[..., Movie]) -> None:
        self._movie_factory = movie_factory

    def find_all(self) -> List[Movie]:
        raise NotImplementedError()


class CsvMovieFinder(MovieFinder):

    def __init__(
            self,
            movie_factory: Callable[..., Movie],
            path: str,
            delimiter: str,
    ) -> None:
        self._csv_file_path = path
        self._delimiter = delimiter
        super().__init__(movie_factory)

    def find_all(self) -> List[Movie]:
        with open(self._csv_file_path) as csv_file:
            csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
            return [self._movie_factory(*row) for row in csv_reader]


class SqliteMovieFinder(MovieFinder):

    def __init__(
            self,
            movie_factory: Callable[..., Movie],
            path: str,
    ) -> None:
        self._database = sqlite3.connect(path)
        super().__init__(movie_factory)

    def find_all(self) -> List[Movie]:
        with self._database as db:
            rows = db.execute('SELECT title, year, director FROM movies')
            return [self._movie_factory(*row) for row in rows]


Fügen Sie den Anbieter sqlite_finderzum Container hinzu und geben Sie ihn als Abhängigkeit für den Anbieter an lister.



Lassen Sie uns bearbeiten containers.py:



"""Containers module."""

from dependency_injector import containers, providers

from . import finders, listers, entities

class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    movie = providers.Factory(entities.Movie)

    csv_finder = providers.Singleton(
        finders.CsvMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.csv.path,
        delimiter=config.finder.csv.delimiter,
    )

    sqlite_finder = providers.Singleton(
        finders.SqliteMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.sqlite.path,
    )

    lister = providers.Factory(
        listers.MovieLister,
        movie_finder=sqlite_finder,
    )


Der Anbieter sqlite_finderist abhängig von Konfigurationsoptionen, die wir noch nicht definiert haben. Lassen Sie uns die Konfigurationsdatei aktualisieren:



Bearbeiten config.yml:



finder:

  csv:
    path: "data/movies.csv"
    delimiter: ","

  sqlite:
    path: "data/movies.db"


Erledigt. Lass uns das PrĂĽfen.



Wir fĂĽhren im Terminal aus:



python -m movies


Du wirst sehen:



Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]


Unsere Anwendung unterstützt beide Datenbankformate: csvund sqlite. Jedes Mal, wenn wir das Format ändern müssen, müssen wir den Code im Container ändern. Wir werden dies im nächsten Abschnitt verbessern.



Anbieterauswahl



In diesem Abschnitt werden wir unsere Anwendung flexibler gestalten.



Sie müssen keine Codeänderungen mehr vornehmen, um zwischen csvund sqliteFormaten zu wechseln . Wir werden einen Schalter basierend auf einer Umgebungsvariablen implementieren MOVIE_FINDER_TYPE:



  • Wenn eine MOVIE_FINDER_TYPE=csvAnwendung die verwendet csv.
  • Wenn eine MOVIE_FINDER_TYPE=sqliteAnwendung die verwendet sqlite.


Der Anbieter hilft uns dabei Selector. Es wählt einen Anbieter basierend auf der Konfigurationsoption ( Dokumentation ).



Lassen Sie uns bearbeiten containers.py:



"""Containers module."""

from dependency_injector import containers, providers

from . import finders, listers, entities


class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    movie = providers.Factory(entities.Movie)

    csv_finder = providers.Singleton(
        finders.CsvMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.csv.path,
        delimiter=config.finder.csv.delimiter,
    )

    sqlite_finder = providers.Singleton(
        finders.SqliteMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.sqlite.path,
    )

    finder = providers.Selector(
        config.finder.type,
        csv=csv_finder,
        sqlite=sqlite_finder,
    )

    lister = providers.Factory(
        listers.MovieLister,
        movie_finder=finder,
    )


Wir haben einen Anbieter erstellt finderund als Abhängigkeit für den Anbieter angegeben lister. Der Anbieter finderwählt zwischen Anbietern csv_finderund sqlite_finderzur Laufzeit. Die Auswahl hängt vom Wert des Schalters ab.



Der Switch ist die Konfigurationsoption config.finder.type. Wenn sein Wert csvvom Anbieter aus dem SchlĂĽssel verwendet wird csv. Ebenso fĂĽr sqlite.



Jetzt mĂĽssen wir den Wert config.finder.typeaus der Umgebungsvariablen lesen MOVIE_FINDER_TYPE.



Lassen Sie uns bearbeiten __main__.py:



"""Main module."""

from .containers import ApplicationContainer


def main():
    container = ApplicationContainer()

    container.config.from_yaml('config.yml')
    container.config.finder.type.from_env('MOVIE_FINDER_TYPE')

    lister = container.lister()

    print(
        'Francis Lawrence movies:',
        lister.movies_directed_by('Francis Lawrence'),
    )
    print(
        '2016 movies:',
        lister.movies_released_in(2016),
    )


if __name__ == '__main__':
    main()


Erledigt.



FĂĽhren Sie die folgenden Befehle im Terminal aus:



MOVIE_FINDER_TYPE=csv python -m movies
MOVIE_FINDER_TYPE=sqlite python -m movies


Die Ausgabe fĂĽr jeden Befehl sieht folgendermaĂźen aus:



Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]


In diesem Abschnitt haben wir den Anbieter kennengelernt Selector. Mit diesem Anbieter können Sie Ihre Anwendung flexibler gestalten. Der Switch-Wert kann von jeder Quelle aus festgelegt werden: Konfigurationsdatei, Wörterbuch, anderer Anbieter.



Tipp: Durch

Überschreiben eines Konfigurationswerts von einem anderen Anbieter können Sie die Konfigurationsüberladung in Ihrer Anwendung ohne einen Neustart implementieren.

Dazu mĂĽssen Sie die Providerdelegation und die .override().



Im nächsten Abschnitt werden wir einige Tests hinzufügen.



Tests



Lassen Sie uns zum Schluss einige Tests hinzufĂĽgen.



Erstellen Sie eine Datei tests.pyin einem Paket movies:



./
├── data/
│   ├── fixtures.py
│   ├── movies.csv
│   └── movies.db
├── movies/
│   ├── __init__.py
│   ├── __main__.py
│   ├── containers.py
│   ├── entities.py
│   ├── finders.py
│   ├── listers.py
│   └── tests.py
├── venv/
├── config.yml
└── requirements.txt


und fĂĽgen Sie die folgenden Zeilen hinzu:



"""Tests module."""

from unittest import mock

import pytest

from .containers import ApplicationContainer


@pytest.fixture
def container():
    container = ApplicationContainer()
    container.config.from_dict({
        'finder': {
            'type': 'csv',
            'csv': {
                'path': '/fake-movies.csv',
                'delimiter': ',',
            },
            'sqlite': {
                'path': '/fake-movies.db',
            },
        },
    })
    return container


def test_movies_directed_by(container):
    finder_mock = mock.Mock()
    finder_mock.find_all.return_value = [
        container.movie('The 33', 2015, 'Patricia Riggen'),
        container.movie('The Jungle Book', 2016, 'Jon Favreau'),
    ]

    with container.finder.override(finder_mock):
        lister = container.lister()
        movies = lister.movies_directed_by('Jon Favreau')

    assert len(movies) == 1
    assert movies[0].title == 'The Jungle Book'


def test_movies_released_in(container):
    finder_mock = mock.Mock()
    finder_mock.find_all.return_value = [
        container.movie('The 33', 2015, 'Patricia Riggen'),
        container.movie('The Jungle Book', 2016, 'Jon Favreau'),
    ]

    with container.finder.override(finder_mock):
        lister = container.lister()
        movies = lister.movies_released_in(2015)

    assert len(movies) == 1
    assert movies[0].title == 'The 33'


Beginnen wir nun mit dem Testen und ĂĽberprĂĽfen Sie die Abdeckung:



pytest movies/tests.py --cov=movies


Du wirst sehen:



platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-2.10.0
collected 2 items

movies/tests.py ..                                              [100%]

---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name                   Stmts   Miss  Cover
------------------------------------------
movies/__init__.py         0      0   100%
movies/__main__.py        10     10     0%
movies/containers.py       9      0   100%
movies/entities.py         7      1    86%
movies/finders.py         26     13    50%
movies/listers.py          8      0   100%
movies/tests.py           24      0   100%
------------------------------------------
TOTAL                     84     24    71%


Wir haben die .override()Provider- Methode verwendet finder. Der Anbieter wird von mock ĂĽberschrieben. Wenn Sie sich an den Anbieter finderwenden , wird das ĂĽberschreibende Modell jetzt zurĂĽckgegeben.



Die Arbeit ist erledigt. Lassen Sie uns nun zusammenfassen.



Fazit



Wir haben eine CLI-Anwendung nach dem Prinzip der Abhängigkeitsinjektion erstellt. Wir haben Dependency Injector als Abhängigkeitsinjektions-Framework verwendet.



Der Vorteil, den Sie mit Dependency Injector erhalten, ist der Container.



Der Container beginnt sich auszuzahlen, wenn Sie die Struktur Ihrer Anwendung verstehen oder ändern müssen. Mit einem Container ist dies einfach, da alle Anwendungskomponenten und ihre Abhängigkeiten explizit an einer Stelle definiert sind:



"""Containers module."""

from dependency_injector import containers, providers

from . import finders, listers, entities


class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    movie = providers.Factory(entities.Movie)

    csv_finder = providers.Singleton(
        finders.CsvMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.csv.path,
        delimiter=config.finder.csv.delimiter,
    )

    sqlite_finder = providers.Singleton(
        finders.SqliteMovieFinder,
        movie_factory=movie.provider,
        path=config.finder.sqlite.path,
    )

    finder = providers.Selector(
        config.finder.type,
        csv=csv_finder,
        sqlite=sqlite_finder,
    )

    lister = providers.Factory(
        listers.MovieLister,
        movie_finder=finder,
    )




Ein Container als Karte Ihrer Anwendung. Sie wissen immer, was von was abhängt.



PS: Fragen und Antworten



In den Kommentaren zum vorherigen Tutorial wurden coole Fragen gestellt: "Warum ist das notwendig?", "Warum brauchen wir ein Framework?", "Wie hilft das Framework bei der Implementierung?"



Ich habe Antworten vorbereitet:



Was ist Abhängigkeitsinjektion?



  • Es ist das Prinzip, das die Kopplung verringert und die Kohäsion erhöht


Warum sollte ich die Abhängigkeitsinjektion verwenden?



  • Ihr Code wird flexibler, verständlicher und besser testbar
  • Sie haben weniger Probleme, wenn Sie verstehen mĂĽssen, wie es funktioniert, oder wenn Sie es ändern mĂĽssen


Wie beginne ich mit der Anwendung der Abhängigkeitsinjektion?



  • Sie beginnen mit dem Schreiben von Code nach dem Prinzip der Abhängigkeitsinjektion
  • Sie registrieren alle Komponenten und ihre Abhängigkeiten im Container
  • Wenn Sie eine Komponente benötigen, erhalten Sie diese aus dem Container


Warum brauche ich dafĂĽr einen Rahmen?



  • Sie benötigen ein Framework, um kein eigenes zu erstellen. Der Objekterstellungscode wird dupliziert und ist schwer zu ändern. Um dies zu vermeiden, benötigen Sie einen Container.
  • Das Framework bietet Ihnen einen Container und Anbieter
  • Anbieter steuern die Lebensdauer von Objekten. Sie benötigen Fabriken, Singletons und Konfigurationsobjekte
  • Der Container dient als Sammlung von Anbietern


Welchen Preis zahle ich?



  • Sie mĂĽssen Abhängigkeiten im Container explizit angeben
  • Das ist zusätzliche Arbeit
  • Es wird sich auszahlen, wenn das Projekt wächst
  • oder 2 Wochen nach Abschluss (wenn Sie vergessen, welche Entscheidungen Sie getroffen haben und wie das Projekt aufgebaut ist)


Abhängigkeitsinjektor-Konzept



DarĂĽber hinaus werde ich das Konzept des Dependency Injector als Framework beschreiben.



Der Abhängigkeitsinjektor basiert auf zwei Prinzipien:



  • Explizit ist besser als implizit (PEP20).
  • Mach keine Magie mit deinem Code.


Wie unterscheidet sich Dependency Injector von anderen Frameworks?



  • Keine automatische VerknĂĽpfung. Das Framework verknĂĽpft Abhängigkeiten nicht automatisch. Introspektion, VerknĂĽpfung nach Argumentnamen und / oder Typen wird nicht verwendet. Weil "explizit ist besser als implizit (PEP20)".
  • Verschmutzt Ihren Anwendungscode nicht. Ihre Anwendung kennt den Abhängigkeitsinjektor nicht und ist von ihm unabhängig. Keine @injectDekorateure, Anmerkungen, Patches oder andere Zaubertricks.


Dependency Injector bietet einen einfachen Vertrag:



  • Sie zeigen dem Framework, wie Objekte gesammelt werden
  • Das Framework sammelt sie


Die Stärke des Abhängigkeitsinjektors liegt in seiner Einfachheit und Unkompliziertheit. Es ist ein einfaches Werkzeug, um ein leistungsfähiges Prinzip zu implementieren.



Was weiter?



Wenn Sie interessiert sind, aber zögern, lautet meine Empfehlung:



Versuchen Sie diesen Ansatz 2 Monate lang. Er ist nicht intuitiv. Es braucht Zeit, um sich daran zu gewöhnen und zu fühlen. Die Vorteile werden spürbar, wenn das Projekt auf mehr als 30 Komponenten in einem Container anwächst. Wenn es Ihnen nicht gefällt, verlieren Sie nicht viel. Wenn es Ihnen gefällt, erhalten Sie einen erheblichen Vorteil.





Ich wĂĽrde mich ĂĽber Feedback und Beantwortung von Fragen in den Kommentaren freuen.



All Articles