Что я должен делать, если метод Moose Builder не работает? - PullRequest
10 голосов
/ 29 января 2010

Каков наилучший способ обработки сбоя в методе компоновщика?

Например:

package MyObj;
use Moose;
use IO::File;

has => 'file_name'   ( is => 'ro', isa => 'Str',      required   =>1  );
has => 'file_handle' ( is => 'ro', isa => 'IO::File', lazy_build => 1 );

sub _build_file_handle {
    my $self = shift;
    my $fh = IO::File->new( $self->file_name, '<' );

    return $fh;
}

Если _build_file_handle не удается получить дескриптор, сборщик вернет undef, что не соответствует ограничению типа.

Я мог бы использовать объединение в ограничении типа file_handle, чтобы оно принимало undef в качестве допустимого значения. Но тогда предикат has_file_handle вернет true, даже если значение равно undef.

Есть ли способ сообщить о том, что построителю не удалось, и атрибут должен оставаться очищенным?

Ответы [ 3 ]

9 голосов
/ 30 января 2010

Вы не думаете на достаточно высоком уровне. ОК, строитель терпит неудачу. Атрибут остается неопределенным. Но что вы делаете с кодом, вызывающим метод доступа? Контракт класса указал, что вызов метода всегда будет возвращать IO :: File. Но сейчас возвращается undef. (Контракт был IO::File, а не Maybe[IO::File], верно?)

Таким образом, на следующей строке кода вызывающий объект умрет («Не удается вызвать метод readline» для неопределенного значения в строке 42__caller.pl), поскольку он ожидает, что ваш класс будет следовать контракту что это определено. Ваш класс не должен был терпеть неудачу, но теперь это произошло. Как звонящий может что-то сделать, чтобы исправить эту проблему?

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

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

Теперь, если вы не готовы умереть во время работы компоновщика, вам нужно измениться, когда код, который может дать сбой, запускается. Вы можете сделать это во время создания объекта, сделав его не ленивым или явно оживив атрибут в BUILD (BUILD { $self->file_name }).

Лучший вариант - вообще не открывать дескриптор файла для внешнего мира, а вместо этого делать что-то вроде:

# dies when it can't write to the log file
method write_log {
    use autodie ':file'; # you want "say" to die when the disk runs out of space, right?
    my $fh = $self->file_handle;
    say {$fh} $_ for $self->log_messages;
}

Теперь вы знаете, когда программа умрет; в new или write_log. Вы знаете, потому что документы так говорят.

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

И смерть в write_log может даже быть чем-то, что вы можете восстановить (в блоке улова), тогда как "не может открыть эту случайную непрозрачную вещь, о которой вы не должны знать в любом случае", вызывающему абоненту гораздо сложнее оправиться от.

По сути, разрабатывайте свой код разумно, и исключения - единственный ответ.

(В любом случае, я не понимаю, что это за «они»). Они точно так же работают в C ++ и очень похожи в Java, Haskell и во всех других языках. Действительно ли слово die действительно страшно или что-то в этом роде? ?)

6 голосов
/ 29 января 2010

«Лучшее» субъективно, но вам придется решить, какой смысл в вашем коде:

  1. если вы можете продолжить работу в своем коде, когда файловый дескриптор не может быть собран (т. Е. Это восстанавливаемое условие), сборщик должен вернуть undef и установить ограничение типа на 'Maybe[IO::File]'. Это означает, что вам также придется проверять определенность этого атрибута при его использовании. Вы также можете проверить, правильно ли построен этот атрибут в BUILD, и выбрать дальнейшие действия в этой точке (как указано в его комментарии), например. вызов clear_file_handle, если он не определен (так как сборщик всегда будет присваивать значение атрибуту, если, конечно, он не умрет).

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

PS. Вы также можете захотеть взглянуть на Try :: Tiny , который Moose использует внутри и является в основном просто оболочкой для * идиомы do eval { blah } or die ....

* Но сделано правильно! и круто! (Кажется, я слышу много шепота на ухо от # лося ..)

2 голосов
/ 29 января 2010

Есть ли способ сообщить о том, что построителю не удалось, и атрибут должен оставаться очищенным?

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

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

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

sub get_fh {                                                                
  my $self = shift;                                                         

  my $abs_loc = $self->abs_loc;                                             

  if ( !(-e $abs_loc) || -e -z $abs_loc ) {                                 
    $self->error({ msg => "Critical doesn't exist or is totally empty" });  
    die "Will not run this, see above error\n";                             
  }                                                                         

  my $st = File::stat::stat($abs_loc);                                      
  $self->report_datetime( DateTime->from_epoch( epoch => $st->mtime ) );    

  my $fh = IO::File->new( $abs_loc, 'r' )                                   
    || die "Can not open $abs_loc : $!\n"                                   
  ;                                                                         

  $fh;                                                                      

}                                                                           

Совершенно другой подход заключается в создании подкласса IO::File с метаданными о файле, который вы хотите сохранить. Иногда это эффективное и отличное решение:

package DM::IO::File::InsideOut;
use feature ':5.10';
use strict;
use warnings;

use base 'IO::File';

my %data;

sub previouslyCreated {
  $data{+shift}->{existed_when_opened}
}

sub originalLoc {
  $data{+shift}->{original_location}
}

sub new {
  my ( $class, @args ) = @_;

  my $exists = -e $args[0] ? 1 : 0;

  my $self = $class->SUPER::new( @args );

  $data{$self} = {
    existed_when_opened => $exists
    , original_location => $args[0]
  };

  $self;

};
...