Добрый день!
Мы все учились понемногу
Чему‑нибудь и как‑нибудь,
Так воспитанием, слава богу, У нас немудрено блеснуть....
А.С. Пушкин.
Язык 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, меня удивил своим подходом. Он отличается от того, как бы я написал на другом языке. В связи с этим я и хотел бы спросить: на сколько это правильное решение?