使程序包同时具有功能性和面向对象

时间:2018-10-11 14:33:11

标签: perl package cpan

我已经看到可以以功能或OO方式使用的CPAN Perl模块。我通常根据需要编写OO和Functional程序包,但是我仍然不怎么写可以同时使用两种方法的模块。

有人可以给我一个可以以功能和/或面向对象方式使用的软件包的简单示例吗?我显然对允许两种包装都使用的件很感兴趣。

谢谢

3 个答案:

答案 0 :(得分:3)

一个核心示例是File::Spec,它有一个File::Spec::Functions包装器。它并不是面向对象的,而是使用了面向对象的继承原理,因此其主要API使用了方法调用,但是不需要保持任何状态。

use strict;
use warnings;
use File::Spec;
use File::Spec::Functions 'catfile';

print File::Spec->catfile('/', 'foo', 'bar');
print catfile '/', 'foo', 'bar';

另一个示例是Sereal,其编码器和解码器既可以用作对象,也可以通过包装它们的导出函数来使用。

use strict;
use warnings;
use Sereal::Encoder 'encode_sereal';

my $data = {foo => 'bar'};

my $encoded = Sereal::Encoder->new->encode($data);
my $encoded = encode_sereal $data;

撇开现实,将对象类和导出模块分开是通常的良好组织习惯。尤其不要尝试使同一个函数可以作为方法或导出函数来调用;主要问题在于,无论该子程序被称为$obj->function('foo')还是function($obj, 'foo'),它都与子例程本身没有区别。正如@choroba指出的那样,CGI.pm试图做到这一点,这是一团糟。

答案 1 :(得分:2)

我的WiringPi::API发行版是这样写的。请注意,在这种情况下,不需要保存状态,因此,如果必须保持状态,则这种保存方式将无法按原样工作。

您可以在功能上使用它:

use WiringPi::API qw(:all)

setup_gpio();
...

或使用其面向对象的界面:

use WiringPi::API;

my $api = WiringPi::API->new;
$api->setup_gpio();
...

对于功能,我使用@EXPORT_OK,以便不会不必要地污染用户的名称空间:

our @EXPORT_OK;

@EXPORT_OK = (@wpi_c_functions, @wpi_perl_functions);
our %EXPORT_TAGS;

$EXPORT_TAGS{wiringPi} = [@wpi_c_functions];
$EXPORT_TAGS{perl} = [@wpi_perl_functions];
$EXPORT_TAGS{all} = [@wpi_c_functions, @wpi_perl_functions];

...以及一些示例函数/方法。本质上,我们检查传入的参数数量,如果有多余的参数(将是类/对象),则手动shift手动将其删除:

sub serial_open {
    shift if @_ > 2;
    my ($dev_ptr, $baud) = @_;
    my $fd = serialOpen($dev_ptr, $baud);
    die "could not open serial device $dev_ptr\n" if $fd == -1;
    return $fd;
}
sub serial_close {
    shift if @_ > 1;
    my ($fd) = @_;
    serialClose($fd);
}
sub serial_flush {
    shift if @_ > 1;
    my ($fd) = @_;
    serialFlush($fd);
}

通常,我会做一些参数检查以确保我们转移了正确的东西,但是在测试中,允许后端C / XS代码为我担心的速度更快。

答案 2 :(得分:1)

如前所述,有许多模块可以做到这一点,其中一些已经被命名。 一个好的做法是为功能接口编写一个单独的模块,该模块use对该类进行操作并照此导出其(选择)功能。

但是,如果有特殊需要,可以在一个程序包中同时使用相同的方法/函数名称来包含两个接口。请参阅最后一节,了解一个非常的罕见案例,以下基本示例将无法处理,以及如何解决。

这是具有两个接口的基本软件包

package Duplicious;  # having interfaces to two paradigms may be confusing

use warnings;
use strict;
use feature 'say';

use Scalar::Util qw(blessed);    
use Exporter qw(import);

our @EXPORT_OK = qw(f1);

my $obj_cache;  # so repeated function calls don't run constructor

sub new {
    my ($class, %args) = @_; 
    return bless { }, $class;
}

sub f1 {
    say "\targs in f1: ", join ', ', @_;  # see how we are called

    my $self = shift;
    # Functional interface
    # (first argument not object or class name in this or derived class)
    if ( not ( (blessed($self) and $self->isa(__PACKAGE__)) 
            or (not ref $self  and $self->isa(__PACKAGE__)) ) )
    { 
        return ($obj_cache || __PACKAGE__->new)->f1($self, @_);
    }   

    # Now method definition goes
    # ...
    return 23;
}

1;

来电者

use warnings;            # DEMO only --
use strict;              # Please don't mix uses in the same program
use feature 'say';

use Duplicious qw(f1);

my $obj = Duplicious->new;

say "Call as class method: ";
Duplicious->f1("called as class method");

say "Call as method:";
my $ret_meth = $obj->f1({}, "called as method");

say "\nCall as function:";
my $ret_func = f1({}, "called as function");

我发现原则上在定义类的模块中使用Exporter很尴尬(但是我不知道这样做有任何实际问题);这会导致潜在的混乱界面。这本身就是分离接口的一个好理由,以便功能性的必须加载特定的模块。

还有一个细节需要引起注意。方法调用

($obj_cache || __PACKAGE__->new)->f1(...)

使用缓存的$obj_cache(如果已经调用了该子项)进行调用。因此,保留了对象的状态,该状态可以在先前对f1的调用中进行或未进行过处理。

在旨在用于非面向对象的上下文中的调用中,这相当重要,应该仔细研究。如果存在问题,请删除该缓存或将其扩展为完整的if语句,在该语句中可以根据需要重置状态。

这两个用途绝对不能在同一程序中混用。

输出

Call as class method: 
    args in f1: Duplicious, called as class method
Call as method:
    args in f1: Duplicious=HASH(0x21b1b48), HASH(0x21a8738), called as method
Call as function:
    args in f1: HASH(0x21a8720), called as function
    args in f1: Duplicious=HASH(0x218ba68), HASH(0x21a8720), called as function

函数调用分派给该方法,因此分两行(注释参数)。


为了测试派生类,我使用最小

package NextDupl;

use warnings;
use strict;
use feature 'say';

use parent 'Duplicious';

1;

并添加到下面的主程序中

# Test with a subclass (derived, inherited class)
my $inh = NextDupl->new;

say "\nCall as method of derived class";
$inh->f1("called as method of derived class");

# Retrieve with UNIVERSAL::can() from parent to use by subclass    
my $rc_orig = Duplicious->can('f1');

say "\nCall via coderef pulled from parent, by derived class";
NextDupl->$rc_orig("called via coderef of parent by derived class");

附加输出是

Call as method of derived class
    args in f1: NextDupl=HASH(0x11ac720), called as method of derived class

Call via coderef pulled from parent, by derived class
    args in f1: NextDupl, called via coderef of parent by derived clas

它包含在注释中出现的使用UNIVERSAL::can的测试。


在评论中提出并讨论了一个具体的限制(我知道)。

想象一下,我们编写了一个方法,该方法将对象(或类名)作为其第一个参数,因此将其作为->func($obj)来调用;更重要的是-这很重要-此方法允许 any 类,因为它的工作方式并不关心它具有的类。这将是非常特殊的,但是它是可能的,并且会引发以下问题。

与该方法相对应的函数调用为func($obj),并且当$obj恰好位于此类的层次结构中时,将导致方法调用->func()错误地

在确定是否是否存在的代码中没有办法消除歧义 它被称为函数或方法,因为它所做的只是看第一个参数。如果它是我们自己层次结构中的一个对象/类,它会确定这是对该对象的方法调用(或类方法调用),在这种情况下是错误的。

模块的作者可以通过两种简单的方法来解决这个问题

  • 不为这种高度特定的方法提供功能界面

  • 给它一个单独的(明确相关的)名称

  • 通过检查第一个参数来决定如何调用我们的if条件是固定的,但仍然为具有该接口的每个方法编写。因此,在此方法中,再检查一个参数:如果第一个是该类的对象/类,而第二个是(任意)对象/类,则为方法调用。 如果第二个参数为可选,则此方法无效。

所有这些都是完全合理的。在行使其定义特征,拥有和使用数据(“属性”)的类中,可能会有一些方法无法转换为函数调用。这是因为单个程序只应使用 one 接口,并且具有函数的状态是没有状态的,因此依赖它的方法将不会运行。 (为此使用缓存对象非常危险。)

因此,人们总是必须仔细决定接口,然后选择。

感谢Grinnz的评论。


请注意,“函数式编程”有一个完全不同的范例,标题有些不清楚。所有这些都与程序性方法中的功能接口有关。