in reply to Re: What are the core points of good procedural software design? (functions, code structuring)
in thread What are the core points of good procedural software design? (functions, code structuring)

Grandfather, I like it. And I'm glad to use classes where I can. However, it seems to me that compartmentalizing off a part of the program into a class doesn't actually help me understand how to better write the necessary functions -- the only difference now is they're "instance methods" and every time they access one of the object's variables they need a "$self->{}" to do it.

That is to say, the following two things look quite similar to me:

  1. A class, with instance variables, and instance methods
  2. A file, with file-scope lexicals, and functions

So, getting back to the original posted question, perhaps I wasn't specific enough. Here's two specific questions:

  1. Should my functions be returning something instead of setting values of globals? (er.. FSL: file-scope lexicals, that is)
  2. Should my functions be taking arguments (specifying necessary data) instead of just looking at FSLs for that data?

(1) If you say "yes" to the first one, then what does that buy me? Instead of setting an FSL somewhere, I'm returning values, and so need to create an FSL to hold the value anyway! Ex:

foobar(); # vs. my $value = foobar();

...and now I'm back to my file containing a bunch of FSLs, only now they're scattered around the file (in front of function calls) instead of all listed at the top of the file.

(2) If you say yes to the 2nd one: my functions need to know lots of things sometimes. I might have to pass in a bunch of arguments to tell it everything it needs to know. It seems ugly to have a function take more than a few arguments. How do you deal with this situation?

  • Comment on Re^2: What are the core points of good procedural software design? (functions, code structuring)
  • Download Code

Replies are listed 'Best First'.
Re^3: What are the core points of good procedural software design? (functions, code structuring)
by GrandFather (Saint) on Jun 23, 2008 at 05:25 UTC

    What the light weight OO buys you up front is a "legal" way of using FSLs. Or actually, rather than using a FSL, you are using an OSP (Object Scope Property). In other words, it doesn't really buy you anything at all on day one. However, it does make it a whole lot easier to refactor your code as it grows. There are a few minor things that may be considered advantages of the OO approach:

    You might gain a little advantage by using setters on the object rather than setting random FSLs in various places - at least you can centralize sanity checking of values.

    You might gain a little advantage by grouping manipulation code for particular related properties together and describe how the properties are related in a POD block above the manipulation members. Using OSPs rather than FSLs removes the need to provide all the FSLs at the top of your script so grouping related stuff in sensible way becomes easier.

    None of these is a big win. For small chunks of code, unless someone has already solved the problem for you, there are seldom any big wins anyway. The best you can do is prepare the ground for later grand development, and that is where light weight OO does have an advantage.

    In fact, if it is possible you may migrate to an OO solution, it's easier to not pass stuff around for your first iteration. When you OO things just delete all the FSLs and change any $FSL to $self->{FSL}. So you could argue that you ought avoid both 1. and 2. for the first cut.


    Perl is environmentally friendly - it saves trees
Re^3: What are the core points of good procedural software design? (functions, code structuring)
by zby (Vicar) on Jun 23, 2008 at 06:57 UTC
    1. Should my functions be returning something instead of setting values of globals? (er.. FSL: file-scope lexicals, that is)

    ...

    (1) If you say "yes" to the first one, then what does that buy me? Instead of setting an FSL somewhere, I'm returning values, and so need to create an FSL to hold the value anyway!

    At least you don't have the globals in the body of functions definitions. You can now analyze the functions in separation from the rest of the code - this is a clear win. The next step is to move more code into the functions - and you'll get mostly global free code.

      Once upon a time, it was realized that human beings couldn't reliably hold an entire program inside their heads at once, so it was necessary to subdivide tasks into subtasks small enough to grasp.

      The trouble with globals to maintain state is that it "breaks encapsulation", you get "action-at-a-distance" between your subtasks, i.e. you don't have something small enough to hold in your head.

      So from there, you get to the idea that we should write "pure functions" where all arguments are explicitly passed in to the routine, and all returns are explicity passed out of it, and there are no "side-effects".

      The trouble is that if you actually try to write large software projects that way it often gets pretty awkward: if you end up passing in a few dozen pieces of information and returning a dozen, that straight-jacket starts feeling pretty tight.

      So, is there some sort of compromise solution? OOP design, with multiple classes is one of them -- but another, very similar one, is proceedural design with multiple modules. These days I have a slight preference for OOP code, but as far as scoping concerns go, the two are close to identical. The main advantage of OOP (in my opinion) is that the namespaces of the things you're using are always labeled by a short alias, i.e. the names of the objects: consider $Some::Hairy::Name::Space::important_variable, vs the Exporter style of important_variable, vs the OOP style of $shns->get_important_variable.

      As to what do do to refactor your hairy code, I'd suggest:

      • If it's all in a script, you need to move it's guts to a module, any kind of module...
      • Now write tests that verify the behavior of the module (it's better to do this on a module rather than a script, especially if you use the perl debugger).
      • Now do a re-write of the module, making the argument passing explicit. I have an idea on how you might do that (see below).
      • As you isolate each sub from accessing globals directly, you should then write additional tests ("unit tests") that verify the behavior of each of them.

      Okay, now here's a possible strategy for getting the globals under control:

      • Move them all into a hash named %global
      • Rewrite all subs so they accept a hashref to %global as a single argument: some_sub( \%global );.
      • Each sub will then need to unpack each value it needs explicitly: my $global = shift; my $some_value = $global->{ some value };. Don't forget the my.
      • Any changes you need to make to %global need to be organized somehow -- to start with maybe just gather them to the bottom of the routines.
      • Now, look at which values each sub needs to unpack. Do you see any patterns? Can you organize the subs into groups that use (and effect) a particular subset of values?
      • Start moving values out of %global into new hashes oriented toward particular purposes.
      • Re-write the effected routines so that they take the new hashes as arguments, rather than %global.
      • A goal here is to minimize the number of hashes you need, but it's okay for a routine to need more than one as an argument.
      After all that, with any luck you'll be in a position to subdivide this module into multiple modules, one centered around each of the hashes. You could even, you know, bless those hashes, and then you've got perl object classes.

      How do you deal with routines that need to work on more than one of the hashes? In the OOP style, you can do things like pass objects into a method as arguments. That just changes the way you unpack the values a little (if you're being good and using accessors).

      All of this work might not be worth it to you of course, but the early stages of what I'm talking about here (moving the code to a module and writing tests) is bound to be helpful even if you don't complete the program.