Как я могу создать объект, производный класс которого неявно указан в свойствах создания?

Я ищу образец для следующего. (Я работаю на Perl, но не думаю, что язык имеет особое значение).

С родительским классом Foo и дочерними Bar, Baz, Bazza.

Одним из методов создания Foo является разбор строки, и часть этой строки будет неявно указывать, какой класс должен быть создан. Так, например, если он начинается с «http:», то это Bar, но если это не так, но содержит «[Date]», тогда это нравится Базу и так далее.

Теперь, если Foo знает обо всех своих дочерних элементах и ​​о том, какая строка является Bar, что такое Baz и т. д., она может вызвать соответствующий конструктор. Но базовый класс не должен знать ничего о своих потомках.

Я хочу, чтобы конструктор Foo мог по очереди проверять своих детей, пока один из них не скажет: «Да, это мое, я создам эту вещь».

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

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

Образец кода:

package Foo;

my @children;

sub _registerChild
{
  push @children, shift();
}

sub newFromString
{
  my $string = shift;
  foreach (@children) {
    my $object = $_->newFromString(@_) and return $object;
  }
  return undef;
}

package Bar;
our @ISA = ('Foo');

Foo::_registerChild(__PACKAGE__);

sub newFromString
{
  my $string = shift;
  if ($string =~ /^http:/i) {
    return bless(...);
  }
  return undef;
}

person Colin Fine    schedule 16.06.2009    source источник


Ответы (4)


Возможно, вы могли бы реализовать это с помощью Module::Pluggable< /а>? Это избавит от необходимости регистрации.

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

Что-то вроде:

package MyClass;
use Module::Pluggable;

sub new
{
    my ($class, @args) = @_;
    for my $plugin ($class->plugins)
    {
       my $object = $plugin->new(@args);
       return $object if $object;
    }
}

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

person Nic Gibson    schedule 16.06.2009
comment
Спасибо. Логически Module::Pluggable — это именно то, что я искал; но он может быть не идеальным для нашей ситуации, потому что он, вероятно, не будет присутствовать на машинах наших пользователей, поэтому нам придется распространять его с нашими собственными модулями. Это также «волшебство» по сравнению с моим регистрационным решением, поэтому оно, вероятно, будет менее понятным. Но спасибо, что обратили на это мое внимание. - person Colin Fine; 16.06.2009
comment
Это своего рода волшебство, но поведение по умолчанию будет загружать классы только в пространстве имен MyClass::Plugin (и это пространство имен легко переопределить). Таким образом, регистрация будет фактически идентична созданию модуля в этом пространстве имен. Это, очевидно, модуль CPAN, но я был бы удивлен, если бы вы могли управлять каким-либо серьезным приложением на Perl в наши дни без CPAN (или большого переизобретения колеса). - person Nic Gibson; 16.06.2009

Кажется, вы пытаетесь сделать так, чтобы один класс был как базовым, так и фабрикой. Не. Используйте 2 отдельных класса. Что-то вроде этого:

package Foo;

package Bar;
use base 'Foo';

package Baz;
use base 'Foo';

package Bazza;
use base 'Foo';

package Factory;
use Bar;
use Baz;
use Bazza;

sub get_foo {
    my ($class, $string) = @_;
    return Bar->try($string) || Baz->try($string) || Bazza->try($string);
}

А затем используйте его как:

my $foo = Factory->get_foo($string);

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

person mpeters    schedule 16.06.2009
comment
При таком подходе у вас даже есть место в коде, которому нужно знать о полном наборе дочерних классов. Я думаю, что это даже хуже, чем решение реестра. Если вы хотите добавить/удалить дочерний класс, вам придется принять тот факт, что какой-то код где-то еще постоянно изменяется. Таким образом, ваша идея не добавляет гибкости против изменений, на что обычно нацелены шаблоны. - person mkoeller; 17.06.2009
comment
@mkoeller, тривиально заставить фабрику автоматически загружать набор классов в определенном пространстве имен, например Module::Pluggable. Обычно я предпочитаю открыто говорить о таких вещах, чтобы избежать сюрпризов, но это скорее вопрос вкуса, а не гибкость против изменений. Гибкость означает, что было бы легко добавить новые подклассы, которые могут обрабатывать эту строку. Это не обязательно означает, что он обрабатывает их автоматически, если они присутствуют в файловой системе. - person mpeters; 17.06.2009

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

Затем класс Foo обнаружит существующие клиентские классы во время выполнения и вызовет их по очереди.

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

person mkoeller    schedule 16.06.2009
comment
Спасибо. Мне кажется, что если я должен сделать что-то подобное, то мой «регистр» проще и понятнее. Я надеялся, что существует установившаяся закономерность, которую я пропустил. - person Colin Fine; 16.06.2009
comment
Решение зависит от варианта использования, который вы хотите рассмотреть. Если вы хотите, чтобы ваша фабрика Foo была открыта для еще неизвестных сторонних дочерних классов, вы выполните поиск. Если вы живете в контролируемой среде, поддерживающей только собственный код, то с реестром все будет в порядке. - person mkoeller; 17.06.2009

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

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

тогда синглтон мог бы управлять созданием всех дочерних классов и их распространением (клонировать их, если они не работают?) ... кроме того, дочерние классы могут быть перемещены в отдельную dll для обеспечения разделения.

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

person John Nicholas    schedule 16.06.2009
comment
Спасибо - это не синглтон, это абстрактный базовый класс. Как только объекты будут созданы, они получат правильные методы с помощью полиморфизма, это то, как справиться со случаем, когда еще нет объекта подкласса, о котором я беспокоился. - person Colin Fine; 16.06.2009