Рубрики
Без рубрики

Питоническое руководство по принципам солидного дизайна

Люди, которые знают меня, скажут вам, что я большой поклонник солидных принципов дизайна, отстаиваемых Робертом … Tagged with Python, Codequality.

Люди, которые знают меня, скажут вам, что я большой поклонник солидных принципов дизайна, отстаиваемых Робертом С. Мартином (дядя Боб). За эти годы я использовал эти принципы в C#, PHP, Node.js и Python. Везде, где я их взял, они в целом были хорошо приняты … за исключением случаев, когда я начал работать в Python. Я продолжал получать комментарии, такие как «это не очень питонический способ делать что -то» во время обзора кода. В то время я был новичком в Python, поэтому я действительно не знал, как ответить. Я не знал, как означал или выглядел Pythonic Code, и ни одно из предлагаемых объяснений не было очень удовлетворительным. Это честно разозлило меня. Я чувствовал, что люди используют Pythonic Way для отгрузки для написания более дисциплинированного кода. С тех пор у меня была миссия доказать, что солидный код является питоническим. Вот и сейчас, вот шаг.

Что такое солидный дизайн

Перья Майкла можно зачислять за создание мнемонического твердого вещества, основанное на принципах бумаги Роберта С. Мартина, «Принципы дизайна и дизайнерские шаблоны». Принципы есть

  • Принцип единственной ответственности
  • Открытый закрытый принцип
  • Принцип заместителя Лискова
  • Принцип сегрегации интерфейса
  • Принцип инверсии зависимости

Мы рассмотрим их более подробно в ближайшее время. Самое важное, что следует отметить о принципах солидного дизайна, – это их целостное использование. Выбор одного, и только один не сделает для вас многое. Это когда используется вместе, вы начинаете видеть реальную ценность в этих принципах.

Что такое питонический путь

Хотя нет официального определения для Pythonic Way Little Googling, дает вам несколько ответов вдоль этого общего тщетного.

«Выше правильный синтаксис Pythonic Code следует общепринятых соглашениям сообщества Python и использует язык таким образом, который следует за философией основания». – Дерек Д.

Я думаю, что самое точное описание исходит от дзен питона.

Дзен Питона, Тим Петерс Красиво лучше уродливого. Явное лучше, чем неявное. Просто лучше, чем сложный. Комплекс лучше, чем сложный. Квартира лучше, чем вложенная. Рубкий лучше, чем плотный. Читабельности. Особые случаи недостаточно особенные, чтобы нарушить правила. Хотя практичность превосходит чистоту. Ошибки никогда не должны проходить молча. Если явно не замолчал. Перед лицом двусмысленности отказ от искушения угадать. Должен быть один-и предпочтительно только один-очевидный способ сделать это. Хотя поначалу это может быть не очевидно, если вы не голландский. Теперь лучше, чем никогда. Хотя никогда не часто лучше Правильно Теперь. Если реализация трудно объяснить, это плохая идея. Если реализация легко объяснить, это может быть хорошей идеей. Пространства имен – это одна отличная идея – давайте сделаем больше из них!

Последняя вещь

Прежде чем я прыгаю прямо в принципы и то, как они относятся к дзену Python, я хочу сделать одну вещь, которую я хочу сделать, что ни один другой солидный учебник не делает. Вместо того, чтобы использовать различный фрагмент кода для каждого принципа, мы будем работать с одной базой кода и сделаем его более надежным, когда мы охватываем каждый принцип. Вот код, с которого мы собираемся начать.

class FTPClient:
  def __init__(self, **kwargs):
    self._ftp_client = FTPDriver(kwargs['host'], kwargs['port'])
    self._sftp_client = SFTPDriver(kwargs['sftp_host'], kwargs['user'], kwargs['pw'])

  def upload(self, file:bytes, **kwargs):
    is_sftp = kwargs['sftp']
    if is_sftp:
      with self._sftp_client.Connection() as sftp:
        sftp.put(file)
    else:
      self._ftp_client.upload(file)

  def download(self, target:str, **kwargs) -> bytes:
    is_sftp = kwargs['sftp']
    if is_sftp:
      with self._sftp_client.Connection() as sftp:
        return sftp.get(target)
    else:
      return self._ftp_client.download(target)

Принцип единственной ответственности (SRP)

Определение: Каждый модуль/класс должен иметь только одну ответственность, и, следовательно, только одна причина измениться.

Соответствующий дзен: Должен быть один … и предпочтительно только один-очевидный способ сделать что-то

Принцип единственной ответственности (SRP) – все о увеличении сплоченности и уменьшении связи путем организации кода вокруг обязанностей. Это не большой скачок, чтобы понять, почему это происходит. Если весь код для любой данной ответственности находится в одном месте, которое является сплоченным, и хотя обязанности могут быть похожими, они не часто перекрываются. Рассмотрим этот пример, не связанный с кодом. Если вы несете ответственность за подметать и мою ответственность за то, чтобы убить, у меня нет причин отслеживать, был ли пол. Я могу просто спросить вас: «Был ли пол»? и основывать мои действия в соответствии с вашим ответом.

Я нахожу полезным думать о обязанностях как о вариантах использования, так и в том, как наше дзен вступает в игру. Каждый вариант использования следует обрабатывать только в одном месте, в свою очередь, создавая один очевидный способ сделать что -то. Это также удовлетворяет «одной из причин изменить» часть определения SRP. Единственная причина, по которой этот класс должен измениться, заключается в том, что вариант использования изменился.

Изучение нашего исходного кода, мы видим, что класс не несет ни одной ответственности, потому что он должен управлять деталями соединения для FTP и SFTP -сервера. Кроме того, методы даже не несут ни одной ответственности, потому что оба должны выбирать, какой протокол они будут использовать. Это можно исправить, разделяя Ftpclient класс в 2 класса каждый с одной из обязанностей.

class FTPClient:
  def __init__(self, host, port):
    self._client = FTPDriver(host, port)

  def upload(self, file:bytes):
    self._client.upload(file)

  def download(self, target:str) -> bytes:
    return self._client.download(target)


class SFTPClient(FTPClient):
  def __init__(self, host, user, password):
    self._client = SFTPDriver(host, username=user, password=password)

  def upload(self, file:bytes):
    with self._client.Connection() as sftp:
      sftp.put(file)

  def download(self, target:str) -> bytes:
    with self._client.Connection() as sftp:
      return sftp.get(target)

Одно быстрое изменение и наш код уже чувствует себя намного более питоническим. Код разрешен, и не плотный, просто не сложный, плоский и не вложенный. Если вы еще не на борту, подумайте о том, как будет выглядеть исходный код с обработкой ошибок по сравнению с кодом после SRP.

Открытый закрытый принцип (OCP)

Определение: Программные объекты (классы, функции, модули) должны быть открыты для расширения, но закрыты для изменений.

Соответствующий дзен: Просто лучше, чем сложный. Комплекс лучше, чем сложный.

Поскольку определение изменений и расширения настолько схожи, что легко перегружено открытым закрытым принципом. Я нашел наиболее интуитивно понятный способ решить, что я внесу изменения или расширение, – это подумать о подписях функций. Изменение – это все, что заставляет обновлять код вызова. Это может быть изменение имени функции, обмена порядок параметров или добавление параметра без декорации. Любой код, который вызывает функцию, будет вынужден измениться в соответствии с новой подписью. Расширение, с другой стороны, допускает новые функциональные возможности, не изменяя вызовный код. Это может быть переименование параметра, добавление нового параметра со значением по умолчанию или добавление *arg , или ** Kwargs параметры. Любой код, который вызывает функцию, все равно будет работать так же, как изначально. Те же правила применяются и к классам.

Вот пример добавления поддержки для объемных операций.

Ваша кишечная реакция, вероятно, добавить upload_bulk и Загрузка_BULK функционирует на Ftpclient класс. К счастью, это также надежный способ справиться с этим вариантом использования.

class FTPClient:
  def __init__(self, host, port):
      ... # For this example the __init__ implementation is not significant

  def upload(self, file:bytes):
      ... # For this example the upload implementation is not significant

  def download(self, target:str) -> bytes:
      ... # For this example the download implementation is not significant

  def upload_bulk(self, files:List[str]):
    for file in files:
      self.upload(file)

  def download_bulk(self, targets:List[str]) -> List[bytes]:
    files = []
    for target in targets:
      files.append(self.download(target))

    return files

В этом случае лучше расширить класс с помощью функций, чем продлить наследство, потому что детский класс BulkftPclient должен был бы изменить подпись функции для загрузки, отражая его возвращение списка байтов, а не просто байтов, нарушая открытый закрытый принцип. как принцип замены Лискова.

Принцип замены Лискова (LSP)

Определение: Если S – подтип T, то объекты типа T могут быть заменены объектами типа S.

Соответствующий дзен: Особые случаи недостаточно особенные, чтобы нарушить правила.

Принцип замены Лискова был первым из основных принципов дизайна, о котором я узнал, и единственным, который я узнал в университете. Может быть, поэтому этот так интуитивен для меня. Простой английский способ сказать это: «Любой дочерний класс может заменить свой родительский класс, не нарушая функциональность».

Возможно, вы заметили все классы клиентов FTP, которые имеют одинаковые функции. Это было сделано намеренно, чтобы они следовали принципу замены Лискова. Объект sftpclient может заменить объект ftpclient, и любой код вызывает загрузить , или Скачать , блаженно не знает.

Другим специализированным случаем передачи файлов FTP является поддержка FTPS (да, FTPS и SFTP разные). Решение этого может быть сложно, потому что у нас есть выбор. Они: 1 Добавить upload_secure и Загрузка_secure функции. 2. Добавьте безопасное флаг через ** Kwargs Анкет 3. Создайте новый класс, FtpsClient , это расширяется Ftpclient Анкет

По причинам, в которые мы попадем во время сегрегации интерфейса, и принципы инверсии зависимости новый класс FTPSClient – это путь.

class FTPClient:
  def __init__(self, host, port):
    ...

  def upload(self, file:bytes):
    ...

  def download(self, target:str) -> bytes:
    ...

class FTPSClient(FTPClient):
    def __init__(self, host, port, username, password):
        self._client = FTPSDriver(host, port, user=username, password=password)

Это именно то, что наследство края предназначено для того, чтобы следить за Лисковами для эффективного полиморфизма. Вы заметите, чем сейчас Ftpclient S может быть заменен на FtpsClient или Sftpclient Анкет Фактически, все 3 взаимозаменяемы, что приводит нас к сегрегации интерфейса.

Принцип сегрегации интерфейса (ISP)

Определение: Клиент не должен зависеть от методов, которые он не использует.

Соответствующий дзен: Читаемость подсчеты и сложные лучше, чем сложный.

В отличие от Лискова, принцип сегрегации интерфейса был последним и самым сложным принципом для меня, чтобы понять. Я всегда приравнивал его к ключевому слову интерфейса, и большинство объяснений для твердого дизайна мало что делают для развеяния этой путаницы. Кроме того, большинство руководств, которые я обнаружил, пытаются разбить все на крошечные интерфейсы чаще всего с одной функцией для каждого интерфейса, потому что «слишком много интерфейсов лучше, чем слишком мало».

Здесь есть 2 проблемы, у первого Python нет интерфейсов, а вторые языки, такие как C# и Java, у которых есть интерфейсы, разбивая их слишком много, всегда заканчиваются тем, что интерфейсы реализуют интерфейсы, которые могут стать сложными и сложными, не является питоническим.

Сначала я хочу изучить слишком маленькую проблему интерфейсов, посмотрев на код C#, затем мы рассмотрим питонический подход к провайдеру. Если вы согласны или просто выбираете доверять мне, что супер маленькие интерфейсы – не лучший способ отделить ваши интерфейсы, не стесняйтесь пропустить Pythonic Solution ниже.

# Warning here be C# code
public interface ICanUpload {
  void upload(Byte[] file);
}

public interface ICanDownload {
  Byte[] download();
}

class FTPClient : ICanUpload, ICanDownload {
  public void upload(Byte[] file) {
    ...
  }

  public Byte[] download(string target) {
    ...
  }
}

Проблема начинается, когда вам нужно указать тип параметра, который реализует интерфейсы Icandownload и Icanupload. Приведенный ниже фрагмент кода демонстрирует проблему.

class ReportGenerator {
  public Byte[] doStuff(Byte[] raw) {
    ...
  } 

  public void generateReport(/*What type should go here?*/ client) {
    raw_data = client.download('client_rundown.csv');
    report = this.doStuff(raw_data);
    client.upload(report);
  }
}

В genateReport Функция подписи вам либо нужно указать бетон Ftpclient Класс как тип параметра, который нарушает принцип инверсии зависимости или создает интерфейс, который реализует как интерфейсы как iCanupload, так и интерфейсы iCandownload. В противном случае объект, который просто реализует Icanupload может быть передано, но не удалось бы загрузить звонок и наоборот только с объектом, реализующим ICandownload интерфейс. Нормальный ответ – создать Iftpclient интерфейс и пусть genateReport Функция зависит от этого.

public interface IFTPClient: ICanUpload, ICanDownload {
    void upload(Byte[] file);
    Byte[] download(string target);
}

Это работает, за исключением того, что мы все еще зависим от клиентов FTP. Что если мы хотим начать хранить наши отчеты в S3?

Питоническое решение

Для меня ISP – это разумный выбор для того, как другие разработчики будут взаимодействовать с вашим кодом. Правильно, это больше связано с I в API и CLI, чем ключевым словом интерфейса. Здесь также «количество читабельности» из дзен питона является движущей силой. Хороший интерфейс будет следовать семантике абстракции и соответствовать терминологии, что делает код более читабельным.

Давайте посмотрим, как мы можем добавить S3Client, так как он имеет ту же семантику загрузки/загрузки, что и Ftpclients . Мы хотим сохранить S3Clients подпись для загрузить и Скачать Последовательно, но это было бы чепухой для нового S3Client Унаследовать от Ftpclient Анкет В конце концов, S3 не является особым случаем FTP. Что общего FTP и S3 имеют общего, так это то, что они являются протоколами передачи файлов, и эти протоколы часто имеют аналогичный интерфейс, как это видно в этом примере. Поэтому вместо наследства от Ftpclient Было бы лучше связать эти классы вместе с абстрактным базовым классом, ближайшей вещью Python к интерфейсу.

Мы создаем FileTransferClient который становится нашим интерфейсом, и все наши существующие клиенты теперь наследуют от этого, а не наследуют от Ftpclient Анкет Это заставляет общий интерфейс и позволяет нам перемещать объемные операции в свой собственный интерфейс, поскольку не каждый протокол передачи файлов будет поддерживать их.

from abc import ABC
class FileTransferClient(ABC):
  def upload(self, file:bytes):
    pass

  def download(self, target:str) -> bytes:
    pass

  def cd(self, target_dir):
    pass


class BulkFileTransferClient(ABC):
  def upload_bulk(self, files:List[bytes]):
    pass

  def download_bulk(self, targets:List[str]):
    pass

Что это дает нам, хотя … Ну это.

class FTPClient(FileTransferClient, BulkFileTransferClient):
  ...

class FTPSClient(FileTransferClient, BulkFileTransferClient):
  ...

class SFTPClient(FileTransferClient, BulkFileTransferClient):
  ...

class S3Client(FileTransferClient):
  ...

class SCPClient(FileTransferClient):
  ...

О чувак! Это хороший код или что. Нам даже удалось втиснуть в Scpclient и сохранил массовые действия как свой собственный миксин. Все это хорошо связано с инъекцией зависимости, методом, используемой для принципа инверсии зависимости.

Принцип инверсии зависимости (DIP)

Определение: Модули высокого уровня не должны зависеть от модулей низкого уровня. Они должны зависеть от абстракций, а абстракции не должны зависеть от деталей, скорее детали должны зависеть от абстракций.

Соответствующий дзен: Явное лучше, чем неявное

Это то, что связывает все это вместе. Все, что мы сделали с другими солидными принципами, было, чтобы добраться до места, где мы больше не зависим от детализации, базового протокола передачи файлов, который используется для перемещения файлов вокруг. Теперь мы можем написать код на наших бизнес -правилах, не привязывая их к конкретной реализации. Наш код удовлетворяет обоим требованиям инверсии зависимости.

Нашим высокоуровневым модулям больше не нужно зависеть от низкоуровневого модуля, такого как FTPClient, SFTPClient или S3Client, вместо этого они зависят от абстракции FileTransferClient. Мы зависим от абстракции перемещения файлов, а не детали того, как перемещаются эти файлы.

Наша абстракция FileTransferClient не зависит от конкретных деталей протокола, и вместо этого эти детали зависят от того, как они будут использоваться через абстракцию (то есть файлы могут быть загружены или загружены).

Вот пример инверсии зависимости на работе.

def exchange(client:FileTransferClient, to_upload:bytes, to_download:str) -> bytes:
    client.upload(to_upload)
    return client.download(to_download)

if __name__ == '__main__':
    ftp = FTPClient('ftp.host.com')
    sftp = FTPSClient('sftp.host.com', 22)
    ftps = SFTPClient('ftps.host.com', 990, 'ftps_user', 'P@ssw0rd1!')
    s3 = S3Client('ftp.host.com')
    scp = SCPClient('ftp.host.com')

    for client in [ftp, sftp, ftps, s3, scp]:
        exchange(client, b'Hello', 'greeting.txt')

Вывод

Там у вас есть солидная реализация, которая также очень питоническая. Я надеюсь, что вы, по крайней мере, согрелись до твердого, если вы этого не сделали, и для тех из вас, кто изучает Python и не уверен, как продолжать писать твердый код, это было полезно. Это, конечно, был курируемым примером, который, как я знал, поддается моим аргументам, но в написании этого я все еще был удивлен, как много изменилось на этом пути. Не каждая проблема будет соответствовать этой точной разбивке, но я старался включить достаточно причин моих решений, чтобы вы могли выбрать самую твердую и питоническую реализацию в будущем.

Если вам не согласны или вам нужны какие -либо разъяснения, оставьте комментарий или @d3r3kdrumm0nd в Твиттере.

Оригинал: “https://dev.to/ezzy1337/a-pythonic-guide-to-solid-design-principles-4c8i”