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?