(How would I) Moosify this!

In my previous post, I asked for critique and suggestions for a Moose alternative to my app's handrolled AUTOLOAD implementation of

  1. constructor attributes overriding same-named object methods, and
  2. constructor attributes overriding object proxy methods

I asked, because this is the most complicated behavior of my OO code, and would be a key issue in porting the code to Moose. I also wanted to know if there would be other benefits to using Moose for this app.

Unlike my earlier OO-related posts, no comments appeared.

After some days, Aristotle replied that he believes my question was Warnock'ed (ignored) because the description lacked specifics.

Here is my attempt to provide them. Since I'm not sure exactly what to include and what to exclude, I'm documenting the process of generating the signal routing specification for a run of the Ecasound audio engine.

Using AUTOLOAD for object proxy and method resolution - Does Moose offer a better alternative?

I've written a couple articles on encapsulation issues in perl OO, especially with regards to home-grown versus full-framework OO.

I am told that finding my own solutions to get the behavior I want may lead to poor (dirty, unreliable) code when a better solution could come through a framework such as Moose.

I rolled my own perl OO for Nama, a multitrack audio recording app I've written. However, I'm ready to consider introducing an OO framework for the sake of learning, code quality, testing and maintenance. Provided that I can find satisfactory ways to solve my problems.

Here's a laundry list of (just two) OO features that I'd need or want to implement in a Mooselike framework. One is short and simple to describe, the other will take several paragraphs.

Inheriting field definitions

Track objects in Nama have 34 attributes, and are the biggest class by far. Track subclasses serve special purposes that somewhat evident in their names: SimpleTrack, CacheRecTrack, MixDownTrack, EditTrack, VersionTrack, MixTrack. I've found it convenient to write the base class so Track subclasses inherit attribute names from ancestor classes.

Update: I see this is default behavior for Moose

Object proxy and attribute overriding

I'd read about AUTOLOAD for years, and wondered what the fuss was about. With my home-grown OO, I found I needed to use it.

Here is what I did.

Does breaking encapsulation indicate the wrong type of hammer?

I've had the pleasure of exchanging several emails with Dave Rolsky, author of a new OO tutorial on the subject of whether to introduce treating objects as the underlying hash reference.

He says he won't include even one example because "it really doesn't send the right message. My revisions to perlobj will explain how to implement accessors using the object as a hash reference."

I was shocked, because I find I need to visualize my object's data structures. Also, in several cases, reading or writing attributes directly allows me to implement some needed functionality.

I gave one example of this in a previous post..

Dave suggested the reason I needed to break encapsulation this way is that

  1. I haven't designed my objects properly or

  2. I haven't taken advantage of a full OO framework.

He concludes that I'm using the same coarse hammer to solve all my programming problems, because I'm simply not aware of better solutions.

Those are interesting criticisms.

The second point is absolutely right: I began my OO experiment with Object::Tiny, adding a setter and other facilities as I needed them. In fact, I am proud of this incremental approach, admittedly chosen for my own learning.

Regarding the first point, "design smarts" for me is an extremely limited resource. So I substitute an iterative process of problem-solving and refactoring.

Is it acceptable to use standard Perl tools for this, or should I be reaching higher, tapping more advanced tools? Suppose I had used Moose. Could I have solved my problems easier, more elegantly, with less stumbling?

Well, for one I would have had to ask for help in the Moose community, whereas with hand-rolled OO, I can understand the entire codebase, even if it doesn't meet certain standards of tidiness, design or even hygiene.

On the other hand, I do want my code to be beautiful, for various aesthic reasons, for ease of testing and maintenance, and also to have a code base that will be sufficiently attractive to other people to hack on.

With the aim of exploring whether I can achieve something better using Moose, Mouse, Moo or other advanced framework, in my next post, I'll give an example of some of my more advanced needs, and the naive, if not neolithic ways I implemented them.

Encapsulation: recommended practice or sacred cow?

In the p5p discussions of Dave Rolsky's new Perl OO tutorial and OO recommendations, Johan Vromans and others have mentioned that in good OO programming, one should not violate encapsulation by directly accessing an objects underlying data structure.

$self->{foo} = "Look Ma! No encapsulation"; # don't do this, they say

In general that is true, but not always, We should avoid absolutist language, especially in tutorials. I'll come to an example of that.

For Nama, I generally use a setter with this syntax:

$self->set(foo => "bar");

The set method (inherited from a parent class based on Object::Tiny) makes sure the key is a legal one.

Because it looks distinct, I'm not likely to use it unless I want really write access to that attribute.

This simple approach allows me to manage object attributes culturally, i.e. without specifying them as read-only or read-write. In an app of 13k lines, the 'set' method appears just 110 times.

But it's still possible to directly modify an object in other ways:

my $self = Object->new( foo => [qw(this is a pen)] );
my $array_ref = $self->foo;
$array_ref->[3] = 'lobster'; 
print $self->as_string # "this is a lobster"

I am living with that.

The other point is that I believe there can be legitimate reasons to violate encapsulation. I recently found a good example. Nama allows you to create a new track object that refers to WAV files with a basename other than the track's own name.

For an ordinary track, the name matches the WAV file:

my $Mixdown = Track->new( name => 'Mixdown'); # associates with Mixdown.wav

Here is a track created by the 'link_track' command, that also associates with Mixdown.wav.

my $song = Track->new( name => 'song', target => 'Mixdown');

Here is a track created by the 'new_region' command, that indirectly associates with the same file.

my $final_song = Track->new( name => 'final_song', target => 'song' )

Here is the code I use so that $final_song->target returns 'Mixdown':

package Track;

sub target {
    my $self = shift;
    my $parent = $Track::by_index{$self->{target}};
    defined $parent && $parent->target || $self->{target};

Now there could be some discussion about my using the track name (as opposed to a Track object) as the value for the 'target' field. In short, this design decision has made it easy to serialize and to debug by dumping objects as YAML.

Regarding encapsulation, the point is that accessing $self->{target} is essential for this code to work.

Accessing the underlying hash provides the behavior I want.

I think it's simplistic to assume that every new user could or should find other ways to achieve this behavior without violating encapsulation.

Our tutorials should respect readers' intelligence. I think we can tell them what is good practice, without prescribing an idealistic straitjacket.

Even if I'm missing an easy solution that doesn't access the hash, why should I be required to find one?

For example, I could do it this way:

package Track;

sub _target{ $_[0]->{target} }

sub target {
    my $self = shift;
    my $parent = $Track::by_index{$self->_target};
    defined $parent && $parent->target || $self->_target;

But the code is no clearer and the first line still violates encapsulation.

I can see the importance of respecting encapsulation in large projects using others' OO libraries, but not all use cases fall into that category.

A Novice Refactors

Having read commentary about coding practices, meditated, contemplated, posted a blog entry about my app and begged for constructive criticism, I found my way into a significant refactoring.

I haven't seen much about the step-by-step process of dividing and modularizing code. Here how I am approching it.

The app already uses OO, but suffers from some 260 global variables and about 6k lines of code in the main namespace.

I broke that code into about 30 different modules, testing after creating each new module, with the help of git for source control, and a couple scripts showing which files and in which subs each variable appears

Most modules still occupy the same namespace, however have access to only the minimum necessary subset of global variables. I did this using a structure like this:


 package main;
 our ( $sampling_frequency,.... )
 sub configure_freq { say "configuring soundcard at $sampling_frequency" }

All subroutines are still in the main namespace, so they work unchanged.

The 'our' declaration tells me exactly what global variables the module touches. So I can look at them and think: Do they need to be in the main namespace? Can they be lexicalized? Can they be passed as arguments?

A next step is to provide more separation by introducing a separate namespace.

  package main;
  our ( $sampling_frequency,....)
  package Audio_engine_setup;
  sub report_freq { say "soundcard is running at $sampling_frequency" }

These subroutines (and those in the main namespace they invoke) will have to be called with the full package affiliation, or be imported into the desired namespace via Exporter and 'use', or be converted to use some kind of OO.

Physically reorganizing the code provides an opportunity to partition functions, to define interfaces and to review provisional design decisions made years ago. Of course greater separation of concerns helps the software to be robust and to be readable. And having the code divided up helps anyone who wants to browse/hack.

How much separation is desirable practical is an open question. Most global variables are indices or configuration variables that are set once and read-accessed. Yes, anywhere it's possible to set them by accident. On the other hand, this is a single app, not a toolkit that will be (ab)used by other programs in an adverse environment.