LINUX.ORG.RU

Динамический полиморфизм по средствам замыканий... или как?

 , ,


0

1

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

Писать кучу реализаций и абстрактный класс не вариант, потому что реализаций реально много (в конечном счете за сотню зашкалит).

Решил попробовать сделать через замыкания (раньше не пробовал на пыхе): http://pastebin.com/cLbzjQf3

Ъ - там нечто вроде:

class SCIBlockElementHD {
	
	protected static $_method;
	protected static $_db_res;
	protected static $_GetElements;
    
	protected $_DeployPictures; 
	protected $_DeployLinks;
	protected $_DeployFiles;
	
	
	function __construct(
			# какие-то параметры
		) {
		
		$this->_method = $Method;
		
		$arSelect = $arSelectFields;
		
		if ($arSelectProps) {
			foreach ($arSelectProps as $prop) {
				$arSelect[] = 'PROPERTY_'.$prop;
			}
		}
		
		$this->_db_res = CIBlockElement::GetList($arOrder, $arFilter, false, $arNav, $arSelect);
		
		# constucte closure
		if ($Method == 'P') {
			$this->_GetElements = function ($db_res, $DeployPictures, $DeployLinks, $DeployFiles) {
				
				while($arElement = $db_res->GetNext()) {
					
					$arElement = $DeployPictures($arElement);
					$arElement = $DeployLinks($arElement);
					$arElement = $DeployFiles($arElement);
					
					$arElements[] = $arElement;
				}
				return $arElements;
			};
		} elseif ($Method == 'F') {
			# какой-то код похожий на предыдущий
		} else { # S
			# какой-то код похожий на предыдущий
		}
		
		# пустые замыкания
		$this->_DeployPictures = function ($element) {return $element;};
		$this->_DeployLinks = function ($element) {return $element;};
		$this->_DeployFiles = function ($element) {return $element;};
		
		return $this;
	}
	#
	
    # метод перегрузки замкания (для "настройки" экземпляра)
	public function DeployPictures() {
		$this->_DeployPictures = function ($element) {
			if(isset($element['PREVIEW_PICTURE'])) {
				$element['PREVIEW_PICTURE'] = (0 < $element['PREVIEW_PICTURE'] ? CFile::GetFileArray($element['PREVIEW_PICTURE']) : false);
			}
			if(isset($element['DETAIL_PICTURE'])) {
				$element['DETAIL_PICTURE'] = (0 < $element['DETAIL_PICTURE'] ? CFile::GetFileArray($element['DETAIL_PICTURE']) : false);
			}
			return $element;
		};
	}
	#
	
	# Целевой метод
	public function GetElements() {
		$clsrElements = $this->_GetElements;
		$DeployPictures = $this->_DeployPictures;
		$DeployLinks = $this->_DeployLinks;
		$DeployFiles = $this->_DeployFiles;
		return $clsrElements($this->_db_res, $DeployPictures, $DeployLinks, $DeployFiles);
	}
	#
	
	
}
#

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

Но это выглядит как говно. Хотя работает относительно шустро.

Можно это как-то сделать менее говённо?

★★★★★

Последнее исправление: Suntechnic (всего исправлений: 1)

потому что реализаций реально много (в конечном счете за сотню зашкалит)

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

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

Если сотня — это гипербола и задача формализуется до нескольких типовых классов с частными настройками, то задача ещё упрощается.

В общем, данных мало, задача слишком абстрактно описана, но проблем не вижу.

KRoN73 ★★★★★
()
Ответ на: комментарий от KRoN73

Сотня - гипербола. Но не большая. Что-то около 30 реализаций.

Там есть три (пока) способа получения данных. И есть три дополнительных действия которые могут выполняться или не выполняться: $DeployPictures, $DeployLinks, $DeployFiles. Действие $DeployPictures имеет реализацию не зависимую от способа создания экземпляра. Остальные зависят. Действия могут быть пустыми (т.е. остаться function ($element) {return $element;};). И вот тут собака и порылась. Я могу написать всего 3 ветвления в конструкторе и по три ветвления в двух методах определяющих замыкания + метод без ветвлений. Или же написать 32 реализации метода. Разница есть. Особенно если потом скажем добавится действие...

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

Suntechnic ★★★★★
() автор топика
Последнее исправление: Suntechnic (всего исправлений: 2)
Ответ на: комментарий от Suntechnic

Там есть три (пока) способа получения данных

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

И есть три дополнительных действия которые могут выполняться или не выполняться

Плюс метод, для выполнения действий, который тоже заряжается одним из трёх классов-акторов.

Я могу написать всего 3 ветвления в конструкторе

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

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

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

Вообще, всё получается очень прозрачно и просто, я часто такое практикую :)

KRoN73 ★★★★★
()
Ответ на: комментарий от Suntechnic

И еще я не совсем понимаю почему внутри замыкания _GetElements мне здесь не доступен $this

В PHP замыкание само не лезет никуда во внешние области видимости. Передать замыканию переменную можно только явно, через use().

KRoN73 ★★★★★
()
Ответ на: комментарий от n04h

Ёпт, это же обсуждалось, что писать 100500 классов ОП не хотеть.
ОП может тогда что-нибудь в духе: // не знаю как у вас

@data = {
  route1 => sub {
    'wow'
  },
  route2 => sub {
    'very dirty'
  },  
}->{$route}->(@params)

anonymous
()
Ответ на: комментарий от KRoN73

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

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

Вообще, я бы сказал что здесь ТС классический ССЗБ которому вперлось «нихочу», поэтому он нагородил замыканий и статиков где можно и неможно и теперь страдает, в то время как все что ему нужно было - сделать нормальный интерфейс фабрики и таскать ссылку на фабрику с собой.

abstract class IOFabric {
    public abstract function GetElements();
    public abstract function DeployLinks($data);
    public abstract function DeployFiles($data);
    public abstract function DeployPictures($data);
}

class SCIBlockElementHD {
    private $ioparent; // instance of IOFabric
    public __construct($ioparent) { $this->ioparent = $ioparent; }
    public function GetElements() { return $this->ioparent->GetElements(); }
    public function DeployLinks() { return $this->ioparent->DeployLinks($this); }
    public function DeployFiles() { return $this->ioparent->DeployFiles($this); }
    public function DeployPictures() { return $this->ioparent->DeployPictures($this); }
}

class FacebookFabric extends IOFabric {
    private $user;
    private $password;
    public __construct($user,$password) { $this->user = $user; $this->password = $password; }
    public function GetElements() { return fbGetElements(); }
    public abstract function DeployLinks($data) { return fbPushLinks($this->user,$this->password,$data); }
    public abstract function DeployFiles($data) { return null; }
    public abstract function DeployPictures($data) { return null; }
}


class LocalFabric extends IOFabric {
    private $folder;
    private $database;
    private $thumbfolder;
    public __construct($folder,$thumbfolder,$dblink) {
        $this->folder = $folder;
        $this->dblink = $dblink;
        $this->thumbfolder = $thumbfolder;
    }
    public function GetElements() { return listFsElements(); }
    public abstract function DeployLinks($data) { return saveLinksToDb($this->dblink,$data); }
    public abstract function DeployFiles($data) { return saveFiles($this->folder,$data); }
    public abstract function DeployPictures($data) { return saveThumbnails($this->thumbfolder,$data); }
}

$fabric1 = new LocalFabric('/tmp/f1','/tmp/f1/thumbnails',new PDO(...));
$fabric2 = new FacebookFabric('john.smith','password');

$obj1 = new SCIlockElementHD(fabric1);
$obj1->DeployLinks();
$obj1->DeployPictures();
$obj1->DeployFiles();

$obj2 = new SCIlockElementHD(fabric2);
$obj2->DeployLinks();
$obj2->DeployPictures();
$obj2->DeployFiles();

no-dashi ★★★★★
()
Ответ на: комментарий от no-dashi

А если совсем приспичит динамики, фабрика тоже может быть стратегией/прокси.

no-dashi ★★★★★
()

Не совсем понятно в чем вопрос, но можно ;)

Трэйты в помощь. Читай про паттерн «Посетитель» в связке с «композитором» кажется.

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

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

Чтиво: Книжка про рефакторинг, SOLID, YAGNI

anonymous
()

Не забудь, что closure не очень удобно searilize - лучше использовать статические конструкции вида (callable):

'Class::staticMethoD'

['Class', 'staticMethoD']

[$that, 'methoD']

anonymous
()
Ответ на: комментарий от no-dashi

А почему тогда просто не передавать сразу экземпляры нужных классов?

Так в нашем случае это одно и то же, если под экземплярами классов ты имеешь в виду уже инициированные объекты геттера/сеттера. Просто в одном случае ты будешь инициировать объект снаружи класса, в другом — внутри. Я обычно предпочитаю инициацию таких вспомогательных классов/хелперов из самого основного класса. Ну, например, у меня есть общий класс представления (view). И мне нужно в наследнике указать конкретный класс-рендерер шаблона. Я указываю только имя нужного класса, а уже даже не фабрика класса, а метод, возвращающий контент, обратится к методу, возвращающему экземпляр шаблонизатора/рендерера, и тот по указанному имени создаст объект, экземпляр класса. Например:

class my_view extends meta_view
{
    var $renderer_class = 'templates_smarty';
}
Всё, этого уже достаточно, чтобы дальше работал механизм, типа:
class meta_view
{
    function renderer()
    {
        return class_factory($this->renderer_class());
    }

    function content()
    {
        return $this->renderer()->fetch($this->template(), $this->data());
    }
}

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

extends: my_admin_meta_itemslist

Одна строка, указывающая на оригинальный класс, от которого наследуется наш. Роутинг автоматический по пути в дереве каталога классов, тип выводимых объектов сгенерируется из имени представления (и в случае YAML оно создаётся тоже из иерархии каталогов + имени файла), заголовок страницы тоже создастся автоматом из имён выводимых объектов, вид, в котором выводится каждая запись, задаётся в классе этой записи и т.п. Случай предельный, нередко требуется некоторое изменение стандартного поведения, но всё равно, даже когда во всём файле класса представления 2 или 3 строки кода — это мой метод. Я очень ленивый, машина должна работать, человек — думать :D

KRoN73 ★★★★★
()
Ответ на: комментарий от KRoN73

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

no-dashi ★★★★★
()
Ответ на: комментарий от no-dashi

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

У меня, как правило все такие «хелперы» не зависят от своего состояния. Т.е. все данные — в модели (и отчасти в представлении — всякие настройки вывода и т.п.). Хелперы ими пользуются, но своих данных не имеют. Стараюсь максимально разделять категории логики/данных.

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

KRoN73 ★★★★★
()

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

в зависимости от состояния экземпляра

Это же про паттерн State!

динамической построчной сборкой функции

А это Template Method.

CARS ★★★★
()
Ответ на: комментарий от KRoN73

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

С точки зрения классического ООП - конечно. Это понятно. Но в данном случае я посчитал это слишком расточительным

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

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

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

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

Suntechnic ★★★★★
() автор топика
Ответ на: комментарий от KRoN73

В PHP замыкание само не лезет никуда во внешние области видимости. Передать замыканию переменную можно только явно, через use().

Даже русским по белому:

Замыкания, определенные внутри класса, по сути такие же, как и определенные вне объекта. Единственное отличие состоит в автоматическом импортировании объекта через переменную $this. Это поведение можно запретить, определив замыкание как статическое.

class Dog
{
    private $_name;
    protected $_color;

    public function __construct($name, $color)
    {
         $this->_name = $name;
         $this->_color = $color;
    }

    public function greet($greeting)
    {
         return function() use ($greeting) {
             echo "$greeting, I am a {$this->_color} dog named 
{$this->_name}.";
         };
    }
}

$dog = new Dog("Rover","red");
$dog->greet("Hello");

Выход:
Hello, I am a red dog named Rover. (Привет, я рыжий пес по имени Ровер.)

Причем в таком виде работает. А если замыкание определить в конструкторе и сунуть в свойство объекта - то нет. Пишет что-то типа «использование $this вне контекста объекта».

Suntechnic ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Трэйты в помощь. Читай про паттерн «Посетитель» в связке с «композитором» кажется.

Почитал. Логика Посетителя примерна та же что и с замыканиями сейчас. Фактически вызов замыкания у меня это и есть accept. Но мысль в целом понятна. Я подумаю. Спасибо.

А «композитор» это наверно «компоновщик» (Composite). Я пока не совсем понял в чем он паттерн. Еще почитаю...

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

Ага. Это набросок. На самом деле там есть еще проксирующий синглтон (и не надо про то, что это антипаттерн) и он сейчас ничего не делает. Часть логики может откачевать в него.

Suntechnic ★★★★★
() автор топика
Ответ на: комментарий от Suntechnic

Но в данном случае я посчитал это слишком расточительным

По объёму кода? Фактически у тебя будет оверхед в несколько символов и пару пустых строк на класс.

По производительности? Перекрытие наследованием в PHP очень дешёвая операция, принципиально не дороже обработки условия.

Потери могут быть только на парсинге файла класса при отсутствии кеша байткода. Но обычно те, кто заботится о быстродействии, кеш включают в первую очередь, а в PHP 5.5 он итак идёт из коробки :)

но мне в начале показалось, что с замыканиями это можно будет сделать изящнее

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

KRoN73 ★★★★★
()
Ответ на: комментарий от Suntechnic

С точки зрения классического ООП - конечно. Это понятно. Но в данном случае я посчитал это слишком расточительным

Я настолько не люблю ООП, что лучше изнасилую свой код?

ya-betmen ★★★★★
()

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

Значит создать столько наследников - сколько нужно и в будущем будет легче сопровождать код

Был бы это тикль там бы все решилось тупо динамической построчной сборкой функции.

Не совсем понял, но
http://ua2.php.net/create_function
?

kiotoze ★★★★
()
Ответ на: комментарий от kiotoze

Значит создать столько наследников - сколько нужно и в будущем будет легче сопровождать код

Да. По видимому это в любом случае самый правильный путь. Тем более что все получается несколько проще чем я думал.

KRoN73, no-dashi вы были оба правы в чем-то - спасибо. Спасибо и анону за компоновщик, а посетитель тут не при делах.

http://ua2.php.net/create_function

Я знаю ;) Да, по функционалу это примерно оно. Но оно медленное и ужасное.

Suntechnic ★★★★★
() автор топика
Ответ на: комментарий от Suntechnic

Компоновщик, часто используется в связке с посетителем. Скачай книжку «Паттерны проектирования» банды четырёх, второе издание. Там есть пример компоновщика и посетителя.

Возможно, стоит посмотреть еще паттерны «Приспособленец» «Интерпретатор» «Мост» «Декоратор» «Стратегия» «Команда» «Абстрактная фабрика»

Но это может избыточно: вопрос в эффективности и минимизации усилий...

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.