【问题标题】:How do I implement dispatch tables in Perl?如何在 Perl 中实现调度表?
【发布时间】:2009-08-15 08:20:51
【问题描述】:

我需要在 Perl 中编写一个与存储相关的应用程序。该应用程序需要将文件从本地机器上传到其他一些存储节点。目前上传方式是FTP,未来可能是bittorrent或者一些未知的超级文件传输方式。

对于每个需要上传的文件,都有一个配置文件,其中定义了文件名、文件上传到的存储节点以及上传时应该使用什么传输方式。

当然,我可以用下面的方法来解决我的问题:

{
  if ( $trans_type == "ftp" ) { ###FTP the FILE}
  if ( $trans_type == "bit" ) { ###BIT the FILE}
  ### etc ###
}

但是即使我在学校学到了基本的OO知识,我仍然觉得这不是一个好的设计。 (题主可能有点误导,如果你觉得我的问题可以用非OO的方案优雅的解决,对我来说还算可以。其实会更好,因为我的OO知识有限。)

那么你们能给我一些一般性的建议吗?当然,如果你也提供一些示例代码,这将是一个很大的帮助。

【问题讨论】:

    标签: perl dispatch-table


    【解决方案1】:

    首先,Perl 中的字符串相等测试是eq,而不是==

    如果你有方法来完成这项工作,比如命名位和 ftp,

    my %proc = (
        bit => \&bit,
        ftp => \&ftp,
    );
    
    my $proc = $proc{$trans_type};
    $proc->() if defined $proc;
    

    【讨论】:

    • 我建议添加更多关于这里发生的事情的描述以防万一,但仍然是一个很好的答案。
    • 不需要定义,因为没有一个 false 值是有效的 coderef。此外,如果在查找表中找不到该方法,您应该发出警告。另一种方法是将所有方法放在一个类中并使用can
    • @Sinan Ünür- 如果 $trans_type eq "fronobulax" 呢?换句话说,他没有预料到或没有预料到的类型?
    • @xcramps 然后你会收到一条神秘的错误消息Undefined subroutine &main:: called at __FILE__ line __LINE__,这比让程序继续在未知状态下运行要好。如果您要检查定义性,那么您应该提供自己的警告或错误。
    • @daotoad:不,不会。也许您正在考虑 if ( $proc{$trans_type}{'process_file'} ) 将不存在的 $proc{$trans_type} 转换为空散列的引用,但仅在散列中查找值不会在那里创建散列元素。
    【解决方案2】:

    您可以为此使用哈希...

    1. 让每个传输方法在哈希中注册自己。您可以执行此 OO(通过调用某个传输方法工厂上的方法)或程序方式(只需将散列设置为包变量,或者如果您不想模块化,甚至可以将其放在主包中)。

      package MyApp::Transfer::FTP;
      $MyApp::TransferManager::METHODS{ftp} = \&do_ftp;
      sub do_ftp { ... }
      1;
      
    2. 每种传输方法都使用一致的 API。也许它只是一个函数,也可能是一个对象接口。

    3. 通过哈希调用传输。

      sub do_transfer {
          # ...
          my $sub = $MyApp::TransferManager::METHODS{$method}
              or croak "Unknown transfer method $method";
          $sub->($arg1, $arg2, ...);
          # ...
      }
      

    顺便说一句:OO 注册方法看起来像这样:

    package MyApp::TransferManager;
    use Carp;
    use strict;
    
    my %registered_method;
    
    sub register {
        my ($class, $method, $sub) = @_;
    
        exists $registered_method{$method}
            and croak "method $method already registered";
    
        $registered_method{$method} = $sub;
    }
    
    # ...
    
    1;
    

    (此代码未经过测试;请原谅缺少分号)

    【讨论】:

    • 哈希仍然存在您列出可能的传输代理的问题。没有理由对这个列表进行硬编码。只需创建 TransferAgent::FTP、TransferAgent::SCP、TransferAgent::BitTorrent 等。然后工厂类可以负责实例化正确的类。
    • @Chas。 Owens:我在哪里对列表进行硬编码?每个方法实现都负责注册自己。让配置文件指定要加载的传输模块相当容易(如果您想要那种级别的自定义,例如,也许您想关闭一个非常依赖的模块)或加载给定目录中的所有 .pm 文件(如果你想要那种级别的魔法)
    • @derobert 各个类如何让自己运行?如果我有一个程序需要传输到多种服务器类型,我是否必须在我的程序中将每种类型指定为单独的use 语句?类在使用之前不能注册自己。这意味着您在某处硬编码给定程序可以使用的类(例如您指出的配置文件)。通过仅在需要时才需要一个类,您不需要那种硬编码。
    • 包变量全局的。
    • @Chas。 Owens:我认为您错过了最后一部分,即在给定目录中加载所有 .pm 文件(例如,将它们视为插件)。以这种方式做事的一个例子:催化剂。即使您确实在配置文件中明确列出,这也不是那么糟糕,因为您已经这样做了(您的配置需要提供连接详细信息)。

      @jrockway 是的,你是对的,他们是。我会修复...
    【解决方案3】:

    这里正确的设计是工厂。看看DBI 是如何处理这个问题的。您最终会得到一个 TransferAgent 类,该类实例化任意数量的 TransferAgent::* 类之一。显然,您将需要比下面提供的实现更多的错误检查。使用这样的工厂意味着您可以添加新类型的传输代理,而无需添加或修改任何代码。

    TransferAgent.pm - 工厂类:

    package TransferAgent;
    
    use strict;
    use warnings;
    
    sub connect {
        my ($class, %args) = @_;
    
        require "$class/$args{type}.pm";
    
        my $ta = "${class}::$args{type}"->new(%args);
        return $ta->connect;
    }
    
    1;
    

    TransferAgent/Base.pm - 包含TransferAgent::* 类的基本功能:

    package TransferAgent::Base;
    
    use strict;
    use warnings;
    
    use Carp;
    
    sub new {
        my ($class, %self) = @_;
        $self{_files_transferred} = [];
        $self{_bytes_transferred} = 0;
        return bless \%self, $class;
    }
    
    sub files_sent { 
        return wantarray ?  @{$_[0]->{_files_sent}} : 
            scalar @{$_[0]->{_files_sent}};
    }
    
    sub files_received { 
        return wantarray ?  @{$_[0]->{_files_recv}} : 
            scalar @{$_[0]->{_files_recv}};
    }
    
    sub cwd    { return $_[0]->{_cwd}       }
    sub status { return $_[0]->{_connected} }
    
    sub _subname {
        return +(split "::", (caller 1)[3])[-1];
    }
    
    sub connect    { croak _subname, " is not implemented by ", ref $_[0] }
    sub disconnect { croak _subname, " is not implemented by ", ref $_[0] }
    sub chdir      { croak _subname, " is not implemented by ", ref $_[0] }
    sub mode       { croak _subname, " is not implemented by ", ref $_[0] }
    sub put        { croak _subname, " is not implemented by ", ref $_[0] }
    sub get        { croak _subname, " is not implemented by ", ref $_[0] }
    sub list       { croak _subname, " is not implemented by ", ref $_[0] }
    
    1;
    

    TransferAgent/FTP.pm - 实现(模拟)FTP 客户端:

    package TransferAgent::FTP;
    
    use strict;
    use warnings;
    
    use Carp;
    
    use base "TransferAgent::Base";
    
    our %modes = map { $_ => 1 } qw/ascii binary ebcdic/;
    
    sub new {
        my $class = shift;
        my $self  = $class->SUPER::new(@_);
        $self->{_mode} = "ascii";
        return $self;
    }
    
    sub connect    { 
        my $self = shift;
        #pretend to connect
        $self->{_connected} = 1;
        return $self;
    }
    
    sub disconnect {
        my $self = shift;
        #pretend to disconnect
        $self->{_connected} = 0;
        return $self;
    }
    
    sub chdir { 
        my $self = shift;
        #pretend to chdir
        $self->{_cwd} = shift;
        return $self;
    }
    
    sub mode {
        my ($self, $mode) = @_;
    
        if (defined $mode) {
            croak "'$mode' is not a valid mode"
                unless exists $modes{$mode};
            #pretend to change mode
            $self->{_mode} = $mode;
            return $self;
        }
    
        #return current mode
        return $self->{_mode};
    }
    
    sub put {
        my ($self, $file) = @_;
        #pretend to put file
        push @{$self->{_files_sent}}, $file;
        return $self;
    }
    
    sub get {
        my ($self, $file) = @_;
        #pretend to get file
        push @{$self->{_files_recv}}, $file;
        return $self;
    }
    
    sub list {
        my $self = shift;
        #pretend to list remote files
        return qw/foo bar baz quux/;
    }
    
    1;
    

    script.pl - 如何使用 TransferAgent:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use TransferAgent;
    
    my $ta = TransferAgent->connect(
        type     => "FTP",
        host     => "foo",
        user     => "bar",
        password => "baz",
    );
    
    print "files to get: ", join(", ", $ta->list), "\n";
    for my $file ($ta->list) {
        $ta->get($file);
    }
    print "files gotten: ", join(", ", $ta->files_received), "\n";
    
    $ta->disconnect;
    

    【讨论】:

    • 我认为您不希望在 FTP 类中使用 use base "TransferAgent" 行。特别是因为您的工厂连接方法在派生类中不起作用(将获得错误的类值,或者更糟糕的是实例)。也许您打算在您的requirenew 行中使用__PACKAGE__
    • 您也可以为此使用 CPAN 中的 Class::Factory。这是一个非常小的模块,但很容易实现和使用。
    • @derobert 是的,已经很晚了,我还没睡。该模式应该有一个单独的类来获取基本功能(这是我打算让 TransferAgent 成为工厂之外的东西)。现在我醒了,我已经更正了代码并充实了一点。
    • @Chris Winters 我以前从未使用过 Class::Factory。它看起来很有趣,但快速浏览似乎说它并不比哈希解决方案更好。看起来它需要您注册可以由它创建的类。在我看来,这违背了使用工厂类的主要原因(即您不需要提前知道可能存在哪些实现)。
    【解决方案4】:

    我在动态子程序部分的Mastering Perl 中有几个示例。

    【讨论】:

      【解决方案5】:
      【解决方案6】:

      OO 将是矫枉过正。我的解决方案可能看起来像这样:

      sub ftp_transfer { ... }
      sub bit_transfer { ... }
      my $transfer_sub = { 'ftp' => \&ftp_transfer, 'bit' => \&bit_transfer, ... };
      ...
      sub upload_file {
          my ($file, ...) = @_;
          ...
          $transfer_sub->{$file->{trans_type}}->(...);
      }
      

      【讨论】:

      • 我相信你需要一个' 在你的哈希子程序上的& 之前,否则我认为Perl 会将&ftp_transfer 返回的值分配给$transfer_sub{ftp},而不是引用子程序。
      • @Chris: \&subname 返回对子名的引用。请参阅 perlref,“制作参考”
      • 拥有一些面向对象的东西很少是矫枉过正的。而这个例子似乎需要面向对象来解决。
      【解决方案7】:

      您最初说过它将使用 FTP,稍后会转移到其他传输方式。在您真正需要添加第二或第三技术之前,我不会变得“优雅”。可能永远不需要第二种转移方法。 :-)

      如果你想把它作为一个“科学项目”来做,那就太好了。

      我厌倦了看到 OO 设计模式使解决永远不会出现的问题的解决方案变得复杂。

      将第一个传输方法包装在一个 uploadFile 方法中。为第二种方法添加 if then else。在第三种方法上获得优雅和重构。到那时,您将拥有足够多的示例,您的解决方案可能会非常通用。

      当然,我的主要观点是第二种和第三种方法可能永远都不需要。

      【讨论】:

      • I-will-make-it-nice-latter 方法的问题在于,当你需要让它变得更好时,有一堆现有的程序正在使用 not-so-漂亮的界面。当然,您必须始终在未来需求与完成任务的简单需求之间取得平衡。在这种情况下,工厂设计模式很容易理解,而且实现起来也相当简单,您将花费很少的时间为未来提供一个好的接口。
      猜你喜欢
      • 2020-01-04
      • 2020-09-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-06
      • 1970-01-01
      • 2012-09-02
      相关资源
      最近更新 更多