Добрый день!

Мы все учились понемногу

Чему‑нибудь и как‑нибудь,

Так воспитанием, слава богу, У нас немудрено блеснуть....

А.С. Пушкин.

Язык Python для меня не является языком программирования, который я использую в повседневной работе. Для меня более близки ООП языки программирования Java, Object Pascal. Поэтому, не холивара ради, я хочу спросить у сообщества на сколько правильно решение, которое я опишу в данной статье.

Для реализации задач CI/CD проекта был реализован класс работы с репозиториями Mercurial:

repo_types = ('git', 'hg')
class Repository:
    """
    Класс работы с репозиторием системы контроля версий
    """
    def __init__(self, name: str, directory: str, repo_type = None):
        if repo_type is None:
            repo_type = 'hg'
        if repo_type not in repo_types:
            raise Exception("Repository type not supported")
	...
    def clone(self, branch_name: str = ""):
    """
    Клонировать репозиторий
    """
    pass
  
    def commit(self, message: str):
    """
    Фиксация изменений в локальном репозитории
    """
    pass
    ...

Через некоторое время перед командой остро встал вопрос перехода на Git. Часть репозиториев переходила на Git, часть оставалась в Mercurial. Причем, это нужно было выполнить «еще год назад».

Для оптимизации времени, был использован не отличающийся оригинальность подход:

    def __init__( self, name: str, directory: str, repo_type = None):
        if repo_type is None:
            repo_type = 'git'
        if repo_type not in repo_types:
            raise Exception("Repository type not supported")
        self.repo_type = repo_type
        ...
    def merge(self, branch_name: str, merge_revision: str):
        """
        Слияние ревизий
        - branch_name: Название ветки - куда вливать
        - merge_revision: Ревизия - что вливать
        """
        if self.repo_type == 'hg':
            ...
        else:
            ...

Итак, для всех методов класса Repository, были реализовано раздельное поведение для Mercurial и Git. Дополнительно были написаны два класса UnitTest'а — TestRepository_HG и TestRepository_Git, которые покрыли юнит тестами все методы класса Repository. Это позволило безболезненно и, в течении короткого времени, перевести основной репозиторий команды в Git.

Но данный код трудно поддерживать и развивать — он становится техническим долгом. Передо мной встал вопрос: «Как оптимально переписать класс Repository, так, что бы весь остальной код, использующий его, остался без изменений?»

Самый напрашивающийся подход — это паттерн Factory. Для Java, в несколько упрощенном виде, код будет выглядеть примерно так:

public abstract class BaseRepository {
    public abstract void clone(String branchName);
    public abstract void commit(String message);
    ...
}

public class HgRepository extends BaseRepository {
    @Override
    public void clone(String branchName) {...}
    @Override
    public void commit(String message) {...}
    ...
}

public class GitRepository extends BaseRepository {
    @Override
    public void clone(String branchName) {...}
    @Override
    public void commit(String message) {...}
    ...
}

public enum RepositoryType {HG, GIT};

public class Repository {
    public static BaseRepository createRepository(RepositoryType type) throws Exception {
      BaseRepository repository;
      switch (type) {
          case HG:
              repository = new HgRepository();
              break;
          case GIT:
              repository = new GitRepository();
              break;
          default:
              throw new Exception("Repository type not supported ");
      }
      return repository;
    }
}

Но данный подход требует четыре файла: базовый класс, реализация Mercurial, реализация для Git и класс фабрика.

Я попробовал в Python объединить класс фабрику и базовый класс. Получился следующий код:)

import os
from abc import abstractmethod

class Repository:
    """
    Класс работы с репозиторием системы контроля версий
    """
    __repo_type_class__: dict = {
        "hg": "RepositoryHg.RepositoryHg",
        "git": "RepositoryGit.RepositoryGit"
    }

    @staticmethod
    def __get_class__(name: str):
        """
        Функция получения класса по имени
        """
        parts = name.split('.')
        module = ".".join(parts[:-1])
        m = __import__( module )
        for comp in parts[1:]:
            m = getattr(m, comp)
        return m

    def __new__(cls, name: str, directory: str, repo_type = None):
        """
        Создание экземпляра объекта
        """
        class_name = cls.__repo_type_class__.get(repo_type)
        if class_name is None:
            raise Exception("Repository type not supported")
        repo_class = Repository.__get_class__(class_name)
        instance = super().__new__(repo_class)
        return instance

    def __init__(self, name: str, directory: str, repo_type = None):
        """
        Инициализация экземпляра объекта
        """
        self.name = name
        self.directory = directory
        self.repo_type = repo_type

    @abstractmethod
    def clone(self, branch_name: str = ""):
    """
    Клонировать репозиторий
    """

    @abstractmethod
    def commit(self, message: str):
    """
    Фиксация изменений в локальном репозитории
    """
    ...
from amtRepository import Repository

class RepositoryHg(Repository):
    """
    Класс работы с репозиторием Mercurial
    """

    def __init__(self, name: str, directory: str, repo_type = 'hg'):
        """
        Инициализация экземпляра объекта Hg
        """
        super().__init__(name, directory, 'hg')
        ...

    def clone(self, branch_name: str = ""):
    """
    Клонировать репозиторий
    """
    ...

    def commit(self, message: str):
    """
    Фиксация изменений в локальном репозитории
    """
    ...

Описание класса RepositoryGit я пропускаю, поскольку он интуитивно понятен.

Код, который получился в Python, меня удивил своим подходом. Он отличается от того, как бы я написал на другом языке. В связи с этим я и хотел бы спросить: на сколько это правильное решение?

Комментарии (5)


  1. Elordis
    00.00.0000 00:00
    +1

    А зачем объединять фабрику и базовый класс? Это же убивает весь смысл происходящего.

    Базовый класс - это абстракция, контракт описывающий свойства своих наследников. Он о деталях реализации не должен знать ничего.

    Фабрика же - это уже конкретная реализация, выдающая конкретные экземпляры конкретных классов.

    Ясное дело, что если вы их вместе скрестите, то получится дичь.


  1. NeZl0i
    00.00.0000 00:00

    В данном случае, думаю, целесообразней применить Builder pattern.


    1. alisichkin Автор
      00.00.0000 00:00

      Как вариант.
      Но я говорил, что во многих модулях есть использование класса репозиторий
      repository = Repository("name", "directory")
      repository.clone()
      ......
      repository.push("new commit")

      мне не хотелось бы менять код - я хотел оставить весь остальной код без изменений.


  1. AcckiyGerman
    00.00.0000 00:00

    Конструкторы в этих двух классах делают совершенно одно и то же за исключением случая, когда вы явно передаёте None (но какой смысл передавать в конструктор None ?):

    class A:
        def __init__(self, repo_type = None):
            if repo_type is None:
                self.repo_type = 'hg'
    
    class B:
        def __init__(self, repo_type = 'hg'):
            self.repo_type = repo_type
    
    
    if __name__=='__main__':
        a = A()
        b = B()
        print(f'a.repo_type: {a.repo_type}, b.repo_type: {b.repo_type}')
        a = A(None)
        b = B(None)
        print(f'a.repo_type: {a.repo_type}, b.repo_type: {b.repo_type}')
    

    python3 test.py
    a.repo_type: hg, b.repo_type: hg
    a.repo_type: hg, b.repo_type: None


  1. alisichkin Автор
    00.00.0000 00:00

    Да согласен, None выглядит не очень хорошо.
    Это из-за того, что я пропустил часть кода - у нас часть репозиториев перешла на Git, часть осталась в Mercurial + я добивался того что бы не модифицировать уже имеющиеся скрипты CI/CD.

    Полный код выглядит так:
    default_repo_type: dict = {
    "repo1": "git",
    "repo2": "hg",
    "repo3": "hg",
    }
    ........
    if repo_type is None:
    repo_type = default_repo_type.get(name)
    ........