Насколько важно указать, реализует ли класс интерфейс в Perl? - PullRequest
5 голосов
/ 21 ноября 2008

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

package Foo::Type::Bar;
sub generate_foo {
    # about 5-100 lines of code
    return stuff here;
}

Итак, вы можете пойти:

my $bar_foo = Foo::Type::Bar->generate_foo;
my $baz_foo = Foo::Type::Baz->generate_foo;

У нас их много, все в одной иерархии Foo::Type::*.

Я думаю, что пакеты должны четко указывать, что они реализуют интерфейс foo_generate, например ::

.
package Foo::Type::Bar;
use base 'Foo::Type';
sub generate_foo {
    ...
    return stuff here;
}

Я думаю, что это хороший стиль кода, гораздо более понятный и понятный для других кодеров, исследующих код. Это также позволяет вам проверить Foo::Type::Bar->isa('Foo::Type'), чтобы увидеть, реализует ли он интерфейс (другие части подсистемы полностью OO).

Мой друг не согласен. Некоторые аргументы, которые он приводит:

  • Foo::Type::* пакеты имеют четкие имена и используются только во внутреннем проекте, поэтому не возникает вопроса о том, реализует ли данный пакет интерфейс
  • пакеты часто маленькие и являются частью автономной подсистемы, и они воспринимаются как командные файлы или файлы conf, а не как тяжелый код Perl OO
  • Perl выражает реализацию через наследование, которое может быть сложным или проблематичным, особенно когда происходит множественное наследование
  • добавление Foo::Type суперкласса не добавляет никакого значения, так как это был бы буквально пустой пакет, используемый только для включения ->isa lookups
  • программное указание реализации интерфейса - это вопрос стиля персонального кода

Является ли один или другой из нас "правым"? Что бы вы сделали?

Редактировать: в примерах переименовано из Foo :: Generator в Foo :: Type

Ответы [ 6 ]

4 голосов
/ 22 ноября 2008

Ну, есть "утка", которую используют другие динамические языки. UNIVERSAL::can выполняет это до такой степени, что другие языки склонны использовать его.

$unknown->doodle() if $unknown->can( 'doodle' );

Название "утка" набирается из идеи, что "если она ходит как утка и говорит как утка ... это утка". Тем не менее, мы не видим, идет ли он «как утка», мы просто видим, что он делает то, что кто-то называет «гулять», потому что это то, что мы называем, что делает утка. Например, объект Tree Walker не " walk " как утка!

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

$path_or_OS_I_dont_know_which->format( "c:", 'y' );

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

if ( $duck->can( 'talk' ) && $duck->can( 'walk' )) { 
    $duck->walk();
    $duck->talk();
}

это нормально, но

if ( $cand->can( 'walk' ) && $cand->can( 'talk' ) && ... && $cand->can( 'gargle' )) {
    ...
}

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

$cand->snicker() if $cand->can( 'snicker' );

(Конечно, узлы это немного очистят.)

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

Совместимость

У меня есть легкий шаблон, который я называю «Совместимый». Фактически нет пакета с таким названием, но если класс реализует то, что делает Date, он помещает его в @ISA;

our @ISA = qw<... Date::Compatible ...>;

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

sub UNIVERSAL::is_one { 
    my ( $self, @args ) = @_;
    foreach my $pkg ( @args ) { 
        return 1 if $self->isa( $pkg );
    }
    return 0;
}

sub UNIVERSAL::is_compatible_with {
    my ( $self, $class ) = @_;
    return $self->is_one( $class, "${class}::Compatible" );
}

По крайней мере, это дает контракт для выполнения и для запроса кода для проверки. Реализация все еще может решить, насколько важно выполнить определенный метод контракта, если он не требователен.

2 голосов
/ 24 ноября 2008

Я думаю, вам следует перейти на Moose, если вы задаете эти вопросы. Там вы сможете определить свой интерфейс, создав роль или класс с соответствующими абстрактными методами.

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

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

2 голосов
/ 22 ноября 2008

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

my $bar_foo = Foo::Type::Bar->generate_foo

вместо

my $bar_foo = Foo::Type::Bar::generate_foo

В первом случае, есть тонкая фанера OO, которая предполагает, что на картах могут быть какие-то OO / наследования; но, глядя на возражения вашего коллеги по добавлению явного наследования от пустого класса, кажется довольно ясным, что generate_foo вызывается не OO-способом и что наследование не ожидается.

Имея это в виду, никто никогда не наберет

if (Foo::Type::Bar->isa('Foo::Type')) { ... }

непосредственно перед

my $bar_foo = Foo::Type::Bar::generate_foo;

поскольку они могут довольно быстро определить, есть ли в Foo :: Type :: Bar функция generate_foo, и они определят это при написании своего кода. Похоже, вы ищете какое-то утверждение «мы делаем правильные вещи», когда на самом деле, если часть кода ошибочно подтверждает это утверждение, оно завершится аварийно при первом запуске.

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

2 голосов
/ 21 ноября 2008

На мой взгляд, это зависит от масштаба и очень субъективно. Чем больше становится система, тем выгоднее создавать метаданные вокруг нее. Если вы ожидаете, что он вырастет, возможно, эти сценарии использования изменятся и будут стоить времени для его реализации. Но Perl, как правило, ориентирован на прагматическое программирование, кодируя только то, что нужно, и ничего более. В твоем случае, зная, что я мало знаю, я бы сказал, оставь все как есть. Если бы это была Java, я бы склонялся в другом направлении, так как Java с самого начала выигрывает от использования интерфейса намного больше.

1 голос
/ 22 ноября 2008

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

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

Я могу утверждать, что

Foo::Bar->generate
Foo::Baz->generate

имеет больше смысла, если возвращаемые объекты зависят в большей степени от их Bar -ности или Baz -ности, чтобы описать их, чем от их Type -ности.

Действительно хороший способ оценить подобные вещи - это сказать себе: «Я бы поставил это на CPAN таким образом? Хотел бы я (потенциально) любой Perl-программист в мире увидеть это?» Если у вас есть какой-либо дискомфорт от этого (полностью пустой класс? Хммм.), Тогда вам следует отступить и пересмотреть.

1 голос
/ 21 ноября 2008

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

С другой стороны, базовый класс может быть полезен как место для хранения вашего POD. И определить некоторые константы ... У вас есть Foo :: Type.pm? Если вы это сделаете и обсудите generate_foo там, тогда вам не понадобится Generator.pm ...

Да, это судебный вызов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...