Структурные шаблоны проектирования в Python

Обзор Это вторая статья из короткой серии, посвященной шаблонам проектирования в Python [/ design-patterns-in-python /]. Шаблоны проектирования конструкций Шаблоны проектирования конструкций используются для сборки нескольких классов в более крупные рабочие структуры. Иногда интерфейсы для работы с несколькими объектами просто не подходят, или вы работаете с устаревшим кодом, который не можете изменить, но нуждаетесь в новой функциональности, или вы просто начинаете замечать, что ваши структуры кажутся неопрятными и чрезмерными, но все элементы кажутся несущественными.

Обзор

Это вторая статья из короткой серии, посвященной шаблонам проектирования в Python .

Структурные шаблоны проектирования

Структурные шаблоны проектирования используются для сборки нескольких классов в более крупные рабочие структуры.

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

Они очень полезны для создания читаемого, поддерживаемого, многоуровневого кода, особенно при работе с внешними библиотеками, устаревшим кодом, взаимозависимыми классами или многочисленными объектами.

В этой статье рассматриваются следующие шаблоны проектирования:

Адаптер

В реальном мире вы можете использовать адаптер для подключения зарядных устройств к разным розеткам во время поездок в другие страны или к другим моделям телефонов. Вы можете использовать их для подключения старого монитора VGA к разъему HDMI на вашем новом ПК.

Шаблон проектирования получил свое название, потому что его цель одна и та же - адаптация одного входа к другому заранее определенному выходу.

Проблема

Допустим, вы работаете над программным обеспечением для отображения изображений, и пока ваши клиенты хотели отображать только растровые изображения . У вас есть полная реализация для вывода, скажем, .png на экран.

Для простоты функциональность выглядит так:

 from abc import ABC, abstractmethod 
 
 class PngInterface(ABC): 
 @abstractmethod 
 def draw(self): 
 pass 
 
 class PngImage(PngInterface): 
 def __init__(self, png): 
 self.png = png 
 self.format = "raster" 
 
 def draw(self): 
 print("drawing " + self.get_image()) 
 
 def get_image(self): 
 return "png" 

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

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

 class SvgImage: 
 def __init__(self, svg): 
 self.svg = svg 
 self.format = "vector" 
 
 def get_image(self): 
 return "svg" 

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

Решение

Чтобы решить эту проблему, мы реализуем класс адаптера. Как и в случае с реальными адаптерами, наш класс принимает доступный извне ресурс ( SvgImage ) и преобразует его в результат, который нам подходит.

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

Опять же, для простоты, мы просто распечатаем "png" , хотя эта функция будет рисовать изображение в реальной жизни.

Адаптер объекта

Адаптер объекта просто обертывает внешний (служебный) класс, предлагая интерфейс, соответствующий нашему собственному (клиентскому) классу. В данном случае сервис предоставляет нам векторную графику, а наш адаптер выполняет растеризацию и рисует получившееся изображение:

 class SvgAdapter(png_interface): 
 def __init__(self, svg): 
 self.svg = svg 
 
 def rasterize(self): 
 return "rasterized " + self.svg.get_image() 
 
 def draw(self): 
 img = self.rasterize() 
 print("drawing " + img) 

Итак, давайте проверим, как работает наш адаптер:

 regular_png = PngImage("some data") 
 regular_png.draw() 
 
 example_svg = SvgImage("some data") 
 example_adapter = SvgAdapter(example_svg) 
 example_adapter.draw() 

Передача regular_png отлично работает для нашей функции graphic_draw() . Однако передача regular_svg не работает. После адаптации regular_svg мы можем использовать его адаптированную форму так же, как мы использовали бы изображение .png

 drawing png 
 drawing rasterized svg 

В нашей функции graphic_draw() не нужно ничего менять. Он работает так же, как и раньше. Мы просто адаптировали ввод для уже существующей функции.

Адаптер класса

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

Чтобы мы могли это сделать, нам нужно каким-то образом проверить, нужно ли нам выполнять преобразование или нет. Чтобы проверить это, мы вводим исключение:

 class ConvertingNonVector(Exception): 
 # An exception used by class_adapter to check 
 # whether an image can be rasterized 
 pass 

И с этим мы можем сделать адаптер класса:

 class ClassAdapter(png_image, svg_image): 
 def __init__(self, image): 
 self.image = image 
 
 def rasterize(self): 
 if(self.image.format == "vector"): 
 return "rasterized " + self.image.get_image() 
 else: 
 raise ConvertingNonVector 
 
 def draw(self): 
 try: 
 img = self.rasterize() 
 print("drawing " + img) 
 except ConvertingNonVector as e: 
 print("drawing " + self.image.get_image()) 

Чтобы проверить, хорошо ли он работает, давайте проверим его на изображениях .png и .svg

 example_png = PngImage("some data") 
 regular_png = ClassAdapter(example_png) 
 regular_png.draw() 
 
 example_svg = SvgImage("some data") 
 example_adapter = ClassAdapter(example_svg) 
 example_adapter.draw() 

Выполнение этого кода приводит к:

 drawing png 
 drawing rasterized svg 

Адаптер объекта или класса?

В общем, вы должны предпочесть использовать объектные адаптеры . Есть две основные причины, чтобы предпочесть его классовой версии, а именно:

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

Мост

Проблема

Большой класс может нарушать принцип единой ответственности, и может потребоваться разделение на отдельные классы с отдельными иерархиями. Это может быть расширено до большой иерархии классов, которую необходимо разделить на две отдельные, но взаимозависимые иерархии.

Например, представьте, что у нас есть классовая структура, включающая средневековые здания. У нас есть wall , tower , stable , mill , house , armory и т. Д. Теперь мы хотели различать их в зависимости от того, из каких материалов они сделаны. Мы могли бы унаследовать каждый класс и создать straw_wall , log_wall , cobblestone_wall стену, limestone_watchtower башню и т. Д.

Кроме того, tower может быть расширена в watchtower , lighthouse и castle_tower .

плохая структураклассов{.ezlazyload}

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

Более того, будет ли limestone_watchtower башня расширять limestone_tower башню и добавлять особенности сторожевой башни или расширять watchtower и добавлять особенности материала?

Решение

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

Нам нужно создать мост между всеми Building и всеми Material чтобы мы могли генерировать их вариации без необходимости определять их как отдельные классы. Поскольку материал можно использовать во многих вещах, Building будет содержать Material качестве одного из полей:

 from abc import ABC, abstractmethod 
 
 class Material(ABC): 
 @abstractmethod 
 def __str__(self): 
 pass 
 
 class Cobblestone(Material): 
 def __init__(self): 
 pass 
 
 def __str__(self): 
 return 'cobblestone' 
 
 class Wood(Material): 
 def __init__(self): 
 pass 
 
 def __str__(self): 
 return 'wood' 

Итак, давайте Building класс Building:

 from abc import ABC, abstractmethod 
 
 class Building(ABC): 
 @abstractmethod 
 def print_name(self): 
 pass 
 
 class Tower(Building): 
 def __init__(self, name, material): 
 self.name = name 
 self.material = material 
 
 def print_name(self): 
 print(str(self.material) + ' tower ' + self.name) 
 
 class Mill(Building): 
 def __init__(self, name, material): 
 self.name = name 
 self.material = material 
 
 def print_name(self): 
 print(str(self.material) + ' mill ' + self.name) 

Теперь, когда мы хотим создать булыжниковую мельницу или деревянную башню, нам не нужны классы CobblestoneMill или WoodenTower Вместо этого мы можем создать экземпляр Mill или Tower и назначить ему любой материал, который нам нужен:

 cobb = Cobblestone() 
 local_mill = Mill('Hilltop Mill', cobb) 
 local_mill.print_name() 
 
 wooden = Wood() 
 watchtower = Tower('Abandoned Sentry', wooden) 
 watchtower.print_name() 

Выполнение этого кода даст:

 cobblestone mill Hilltop Mill 
 wooden tower Abandoned Sentry 

Композитный

Проблема

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

Это не так просто, как просто запустить цикл, потому что структура каждого блока нерегулярна. Конечно, вы можете перебирать элементы внутри, но что произойдет, если в коробке есть другая коробка с элементами внутри? Как ваша петля может с этим справиться?

Конечно, вы можете проверить класс каждого зацикленного элемента, но это только усложняет задачу. Чем больше у вас классов, тем больше крайних случаев, что приводит к немасштабируемой системе.

Решение

Что примечательно в подобных задачах, так это то, что они имеют древовидную иерархическую структуру. У вас самая большая коробка наверху. И тогда у вас есть более мелкие предметы или внутри коробки. Хороший способ справиться с такой структурой - иметь объект непосредственно над ним, контролируя поведение тех, кто находится под ним.

Шаблон проектирования Composite используется для создания древовидных структур и обработки коллекций объектов аналогичным образом.

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

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

Мы определим абстрактный item класса, что все наши конкретные пункты наследуют от:

 from abc import ABC, abstractmethod 
 
 class Item(ABC): 
 @abstractmethod 
 def return_price(self): 
 pass 

Теперь давайте определим некоторые продукты, которые наши поставщики могут отправлять через нашу компанию:

 class Box(Item): 
 def __init__(self, contents): 
 self.contents = contents 
 
 def return_price(self): 
 price = 0 
 for item in self.contents: 
 price = price + item.return_price() 
 return price 
 
 class Phone(Item): 
 def __init__(self, price): 
 self.price = price 
 
 def return_price(self): 
 return self.price 
 
 class Charger(Item): 
 def __init__(self, price): 
 self.price = price 
 
 def return_price(self): 
 return self.price 
 
 class Earphones(Item): 
 def __init__(self, price): 
 self.price = price 
 
 def return_price(self): 
 return self.price 

Сам Box также является Item и мы можем добавить экземпляр Box внутри экземпляра Box Давайте создадим экземпляры нескольких элементов и поместим их в коробку, прежде чем получить его значение:

 phone_case_contents = [] 
 phone_case_contents.append(Phone(200)) 
 phone_case_box = Box(phone_case_contents) 
 
 big_box_contents = [] 
 big_box_contents.append(phone_case_box) 
 big_box_contents.append(Charger(10)) 
 big_box_contents.append(Earphones(10)) 
 big_box = Box(big_box_contents) 
 
 print("Total price: " + str(big_box.return_price())) 

Выполнение этого кода приведет к:

 Total price: 220 

Декоратор

Проблема

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

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

Сначала вы можете подумать об использовании наследования для решения этой проблемы. В конце концов, если у вас есть basic_player , вы можете унаследовать от него blazing_player , bouncy_player и bowman_player .

Но как насчет blazing_bouncy_player , bouncy_bowman_player , blazing_bowman_player и blazing_bouncy_bowman_player ?

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

Решение

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

Он может вызвать метод из объекта-члена, а затем просто добавить некоторые свои собственные функции поверх него или полностью переопределить его. Затем декоратор (оболочка) можно обернуть другим декоратором, который работает точно так же.

Таким образом, мы можем украшать объект столько раз, сколько захотим, не меняя ни единого бита исходного класса. Давайте продолжим и определим PlayerDecorator :

 from abc import ABC, abstractmethod 
 
 class PlayerDecorator(ABC): 
 @abstractmethod 
 def handle_input(self, c): 
 pass 

А теперь давайте определим BasePlayer с некоторым поведением по умолчанию и его подклассы, указав другое поведение:

 class BasePlayer: 
 def __init__(self): 
 pass 
 
 def handle_input(self, c): 
 if c=='w': 
 print('moving forward') 
 elif c == 'a': 
 print('moving left') 
 elif c == 's': 
 print('moving back') 
 elif c == 'd': 
 print('moving right') 
 elif c == 'e': 
 print('attacking ') 
 elif c == ' ': 
 print('jumping') 
 else: 
 print('undefined command') 
 
 class BlazingPlayer(PlayerDecorator): 
 def __init__(self, wrapee): 
 self.wrapee = wrapee 
 
 def handle_input(self, c): 
 if c == 'e': 
 print('using fire ', end='') 
 
 self.wrapee.handle_input(c) 
 
 class BowmanPlayer(PlayerDecorator): 
 def __init__(self, wrapee): 
 self.wrapee = wrapee 
 
 def handle_input(self, c): 
 if c == 'e': 
 print('with arrows ', end='') 
 
 self.wrapee.handle_input(c) 
 
 class BouncyPlayer(PlayerDecorator): 
 def __init__(self, wrapee): 
 self.wrapee = wrapee 
 
 def handle_input(self, c): 
 if c == ' ': 
 print('double jump') 
 else: 
 self.wrapee.handle_input(c) 

BasePlayer их один за другим, начав с BasePlayer:

 player = BasePlayer() 
 player.handle_input('e') 
 player.handle_input(' ') 

Выполнение этого кода вернет:

 attacking 
 jumping 

Теперь давайте обернем его другим классом, который по-другому обрабатывает эти команды:

 player = BlazingPlayer(player) 
 player.handle_input('e') 
 player.handle_input(' ') 

Это вернет:

 using fire attacking 
 jumping 

Теперь добавим характеристики BouncyPlayer

 player = BouncyPlayer(player) 
 player.handle_input('e') 
 player.handle_input(' ') 

 using fire attacking 
 double jump 

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

 player = BowmanPlayer(player) 
 player.handle_input('e') 
 player.handle_input(' ') 

Это возвращает:

 with arrows using fire attacking 
 double jump 

Фасад

Проблема

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

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

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

Итак, как сделать вашу сложную систему доступной для тех, кто мало знает теорию игр и меньше - о вашей конкретной реализации какой-либо проблемы?

Решение

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

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

Допустим, следующий фрагмент кода - наша «сложная система». Вы, естественно, можете пропустить его чтение, поскольку дело в том, что вам не нужно знать подробности, чтобы использовать его:

 class Hawk: 
 def __init__(self): 
 self.asset = '(`A´)' 
 self.alive = True 
 self.reproducing = False 
 
 def move(self): 
 return 'deflect' 
 
 def reproduce(self): 
 return hawk() 
 
 def __str__(self): 
 return self.asset 
 
 class Dove: 
 def __init__(self): 
 self.asset = '(๑•́ω•̀)' 
 self.alive = True 
 self.reproducing = False 
 
 def move(self): 
 return 'cooperate' 
 
 def reproduce(self): 
 return dove() 
 
 def __str__(self): 
 return self.asset 
 
 def iteration(specimen): 
 half = len(specimen)//2 
 spec1 = specimen[:half] 
 spec2 = specimen[half:] 
 
 for s1, s2 in zip(spec1, spec2): 
 move1 = s1.move() 
 move2 = s2.move() 
 
 if move1 == 'cooperate': 
 # both survive, neither reproduce 
 if move2 == 'cooperate': 
 pass 
 # s1 dies, s2 reproduces 
 elif move2 == 'deflect': 
 s1.alive = False 
 s2.reproducing = True 
 elif move1 == 'deflect': 
 # s2 dies, s1 reproduces 
 if move2 == 'cooperate': 
 s2.alive = False 
 s1.reproducing = True 
 # both die 
 elif move2 == 'deflect': 
 s1.alive = False 
 s2.alive = False 
 
 s = spec1 + spec2 
 s = [x for x in s if x.alive == True] 
 
 for spec in s: 
 if spec.reproducing == True: 
 s.append(spec.reproduce()) 
 spec.reproducing = False 
 
 return s 

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

 import random 
 
 class Simulation: 
 def __init__(self, hawk_number, dove_number): 
 self.population = [] 
 for _ in range(hawk_number): 
 self.population.append(hawk()) 
 for _ in range(dove_number): 
 self.population.append(dove()) 
 random.shuffle(self.population) 
 
 def iterate(self): 
 self.population = iteration(self.population) 
 random.shuffle(self.population) 
 
 def get_assets(self): 
 return [str(x) for x in population] 

Любопытный читатель может поиграться с вызовом iterate() и посмотреть, что происходит с населением.

Наилегчайший вес

Проблема

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

 class Bullet: 
 def __init__(self, x, y, z, velocity): 
 self.x = x 
 self.y = y 
 self.z = z 
 self.velocity = velocity 
 self.asset = '■■►' 

Это заняло бы много памяти, особенно если в воздухе одновременно летит много пуль (и мы не будем сохранять смайлик Unicode вместо ресурсов в реальной жизни).

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

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

Решение

Шаблон « Легковес» требует общего пула, когда может существовать множество экземпляров объекта с одинаковым значением. Известной его реализацией является Java String Pool - где, если вы пытаетесь создать экземпляры двух разных строк с одинаковым значением, создается только одна, а другая просто ссылается на первую.

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

Что мы можем сделать, так это разделить эти черты, чтобы все внутренние черты хранились в одном экземпляре - классе Flyweight . Внешние признаки находятся в отдельных экземплярах, называемых классами контекста . Класс Flyweight обычно содержит все методы исходного класса и работает, передавая им экземпляр класса Context .

Чтобы программа работала должным образом, класс Flyweight должен быть неизменным. Таким образом, если он вызывается из разных контекстов, неожиданного поведения не будет.

Для практического использования часто используется фабрика наилегчайшего веса. Это класс, который при передаче внутреннего состояния проверяет, существует ли уже объект с таким состоянием, и возвращает его, если он существует. Если это не так, он создает новый объект и возвращает его:

 class BulletContext: 
 def __init__(self, x, y, z, velocity): 
 self.x = x 
 self.y = y 
 self.z = z 
 self.velocity = velocity 
 
 class BulletFlyweight: 
 def __init__(self): 
 self.asset = '■■►' 
 self.bullets = [] 
 
 def bullet_factory(self, x, y, z, velocity): 
 bull = [b for b in self.bullets if bx==x and by==y and bz==z and b.velocity==velocity] 
 if not bull: 
 bull = bullet(x,y,z,velocity) 
 self.bullets.append(bull) 
 else: 
 bull = bull[0] 
 
 return bull 
 
 def print_bullets(self): 
 print('Bullets:') 
 for bullet in self.bullets: 
 print(str(bullet.x)+' '+str(bullet.y)+' '+str(bullet.z)+' '+str(bullet.velocity)) 

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

Имея это в виду, давайте воспользуемся bullet_factory() чтобы создать несколько маркеров и распечатать их значения:

 bf = BulletFlyweight() 
 
 # adding bullets 
 bf.bullet_factory(1,1,1,1) 
 bf.bullet_factory(1,2,5,1) 
 
 bf.print_bullets() 

Это приводит к:

 Bullets: 
 1 1 1 1 
 1 2 5 1 

Теперь давайте попробуем добавить больше маркеров через фабрику, которая уже существует:

 # trying to add an existing bullet again 
 bf.bullet_factory(1,1,1,1) 
 bf.print_bullets() 

Это приводит к:

 Bullets: 
 1 1 1 1 
 1 2 5 1 

Прокси

Проблема

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

Это всего лишь один пример - шаблон прокси на самом деле можно использовать в самых разных обстоятельствах, в том числе:

  • Обработка доступа к дорогостоящему объекту, например удаленному серверу или базе данных.
  • Замещение объектов, инициализация которых может быть дорогостоящей до тех пор, пока они действительно не понадобятся программе, например текстуры, которые занимают много места в ОЗУ или большую базу данных.
  • Управление доступом в целях безопасности

Решение

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

PatientFileManager класс PatientFileManager:

 class PatientFileManager: 
 def __init__(self): 
 self.__patients = {} 
 
 def _add_patient(self, patient_id, data): 
 self.__patients[patient_id] = data 
 
 def _get_patient(self, patient_id): 
 return self.__patients[patient_id] 

Теперь давайте сделаем для этого прокси:

 class AccessManager(PatientFileManager): 
 def __init__(self, fm): 
 self.fm = fm 
 
 def add_patient(self, patient_id, data, password): 
 if password == 'sudo': 
 self.fm._add_patient(patient_id, data) 
 else: 
 print("Wrong password.") 
 
 def get_patient(self, patient_id, password): 
 if password == 'totallytheirdoctor' or password == 'sudo': 
 return self.fm._get_patient(patient_id) 
 else: 
 print("Only their doctor can access this patients data.") 

Вот у нас есть пара чеков. Если для прокси-сервера предоставлен правильный AccessManager экземпляр AccessManager может добавлять или получать информацию о пациенте. Если пароль неправильный - не может.

Теперь давайте AccessManager экземпляр AccessManager и добавим пациента:

 am = AccessManager(PatientFileManager()) 
 am.add_patient('Jessica', ['pneumonia 2020-23-03', 'shortsighted'], 'sudo') 
 
 print(am.get_patient('Jessica', 'totallytheirdoctor')) 

Это приводит к:

 ['pneumonia 2020-23-03', 'shortsighted'] 

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

Заключение

При этом все шаблоны структурного проектирования в Python полностью покрыты с рабочими примерами.

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus