a Factory class that returns its own descendants

Good afternoon

We all learned a little

Something and somehow

So education, thank God, It is not surprising for us to shine ….

A.S. Pushkin.

The Python language for me is not a programming language that I use in my daily work. For me, the OOP programming languages ​​Java, Object Pascal are closer. Therefore, not for the sake of holivar, I want to ask the community how correct the solution that I will describe in this article is.

To implement the tasks of the CI / CD project, a class for working with Mercurial repositories was implemented:

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

After some time, the team faced the issue of switching to Git. Some of the repositories switched to Git, some remained in Mercurial. Moreover, this should have been done “a year ago”.

To optimize the time, a non-original approach was used:

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

So, for all methods of the Repository class, separate behavior was implemented for Mercurial and Git. Additionally, two UnitTest classes were written – TestRepository_HG and TestRepository_Git, which covered all methods of the Repository class with unit tests. This made it possible to painlessly and, within a short time, transfer the main team repository to Git.

But this code is difficult to maintain and develop – it becomes technical debt. The question arose before me: “How to optimally rewrite the Repository class, so that all other code that uses it remains unchanged?”

The most obvious approach is the Factory pattern. For Java, in a somewhat simplified form, the code would look something like this:

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;
    }
}

But this approach requires four files: a base class, a Mercurial implementation, a Git implementation, and a factory class.

I tried in Python to combine a factory class and a base class. Got the following code 🙂

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):
    """
    Фиксация изменений в локальном репозитории
    """
    ...

I skip the description of the RepositoryGit class because it is intuitive.

The code that turned out in Python surprised me with its approach. It is different from how I would write in another language. In this regard, I would like to ask: how much is the right decision?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *