Добрый день!
Мы все учились понемногу
Чему‑нибудь и как‑нибудь,
Так воспитанием, слава богу, У нас немудрено блеснуть....
А.С. Пушкин.
Язык 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)
NeZl0i
00.00.0000 00:00В данном случае, думаю, целесообразней применить Builder pattern.
alisichkin Автор
00.00.0000 00:00Как вариант.
Но я говорил, что во многих модулях есть использование класса репозиторий
repository = Repository("name", "directory")
repository.clone()
......
repository.push("new commit")мне не хотелось бы менять код - я хотел оставить весь остальной код без изменений.
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
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)
........
Elordis
А зачем объединять фабрику и базовый класс? Это же убивает весь смысл происходящего.
Базовый класс - это абстракция, контракт описывающий свойства своих наследников. Он о деталях реализации не должен знать ничего.
Фабрика же - это уже конкретная реализация, выдающая конкретные экземпляры конкретных классов.
Ясное дело, что если вы их вместе скрестите, то получится дичь.