views:

112

answers:

8

An Example

Suppose we have a text to write and could be converted to "uppercase or lowercase", and can be printed "at left, center or right".

Specific case implementation (too many functions)

writeInUpperCaseAndCentered(char *str){//..}
writeInLowerCaseAndCentered(char *str){//..}
writeInUpperCaseAndLeft(char *str){//..}
and so on...

vs

Many Argument function (bad readability and even hard to code without a nice autocompletion IDE)

write( char *str , int toUpper, int centered ){//..}

vs

Context dependent (hard to reuse, hard to code, use of ugly globals, and sometimes even impossible to "detect" a context)

writeComplex (char *str)
{    
    // analize str and perhaps some global variables and 
    // (under who knows what rules) put it center/left/right and upper/lowercase
}

And perhaps there are others options..(and are welcome)

The question is:

Is there is any good practice or experience/academic advice for this (recurrent) trilemma ?

EDIT:

What I usually do is to combine "specific case" implementation, with an internal (I mean not in header) general common many-argument function, implementing only used cases, and hiding the ugly code, but I don't know if there is a better way that I don't know. This kind of things make me realize of why OOP was invented.

+3  A: 

I prefer the argument way.

Because there's going to be some code that all the different scenarios need to use. Making a function out of each scenario will produce code duplication, which is bad.

Instead of using an argument for each different case (toUpper, centered etc..), use a struct. If you need to add more cases then you only need to alter the struct:

typedef struct {
    int toUpper;
    int centered;
    // etc...
} cases;
write( char *str , cases c ){//..}
Luca Matteis
could you argue in favor of arguments?
Hernán Eche
Solid... this is the best of all worlds, really, keeping argument count low and not relying on globals. +1
Platinum Azure
After a little though I realised that this is *DANGEROUS*. When you add a new kind of formatting you need to find all instances where you call write and deliberately set the value for this paramater in the now larger struct. If you don't the setting for the new option will be random. Beside, wouldn't it be a pain to have to define and or initialize a structure each time you call the write function?
torak
@torak: Make the cases struct an opaque type---always initialized when you ask for a new one---in the first place, and you only have to fix the initialization routine. You'll find this approach in several places in the traditional unix API and it works well.
dmckee
@dmckee: Can you give an example? Surely if the structure is opaque you can no longer access it to do things like set/clear `toUpper`? That is unless there's set of functions to do that for you.
torak
@torak: The `Write` function must of course have access to the internals, but you don't expose them in the header (i.e. information hiding). If you want mutability, you provide functions, though folks often choose not to allow it (compare to the OOP practice of preferring non-mutable objects in many cases). The c standard `FILE` handle is an example of such a widget as are the BSD `glob_t` and `regex_t` structures. To a lesser extent the `tm` time structure is like this (you know what's in it, but not you're expected to build it yourself: you ask the API).
dmckee
To extend my comment on the question itself: you *can* use all the nice practices encouraged in OOP when doing pure procedural programming, you just don't have the nice syntactic sugar to enforce it and have to use self-discipline instead.
dmckee
@dcmkee: Sure, all of that is true enough. However, given that the intention is use the structure to set various options for various calls to `write` the overhead (both in code and computation) seems a little extreme.
torak
A: 

The 3 arguments one.

Mau
+7  A: 

I'd avoid your first option because as you say the number of function you end up having to implement (though possibly only as macros) can grow out of control. The count doubles when you decide to add italic support, and doubles again for underline.

I'd probably avoid the second option as well. Againg consider what happens when you find it necessary to add support for italics or underlines. Now you need to add another parameter to the function, find all of the cases where you called the function and updated those calls. In short, anoying, though once again you could probably simplify the process with appropriate use of macros.

That leaves the third option. You can actually get some of the benefits of the other alternatives with this using bitflags. For example

#define WRITE_FORMAT_LEFT   1
#define WRITE_FORMAT_RIGHT  2
#define WRITE_FORMAT_CENTER 4
#define WRITE_FORMAT_BOLD   8
#define WRITE_FORMAT_ITALIC 16
....
write(char *string, unsigned int format)
{
  if (format & WRITE_FORMAT_LEFT)
  {
     // write left
  }

  ...
}

EDIT: To answer Greg S.

I think that the biggest improvement is that it means that if I decide, at this point, to add support for underlined text I it takes two steps

  1. Add #define WRITE_FORMAT_UNDERLINE 32 to the header
  2. Add the support for underlines in write().

At this point it can call write(..., ... | WRITE_FORMAT_UNLDERINE) where ever I like. More to the point I don't need to modify pre-existing calls to write, which I would have to do if I added a parameter to its signature.

Another potential benefit is that it allows you do something like the following:

#define WRITE_ALERT_FORMAT  (WRITE_FORMAT_CENTER | \
                             WRITE_FORMAT_BOLD |   \
                             WRITE_FORMAT_ITALIC)
torak
yes, a single global "status" variable with bitflags, I've used that sometimes.
Hernán Eche
I don't see how using bitflags is different from using some other form of arguments. What exactly are the benefits you talk of?
Greg S
@Greg S: Good question, adding explanation to answer.
torak
@Greg @torak It's exactly as using a struct but bitflags are useful when working with embedded microcontrollers because memory usage, of course it's limited to data wide, for example unsigned int could be 16 bit or 32 bit, and that's the total of "yes-no" flags, very limited
Hernán Eche
This is also a traditional approach, and it *can* be made to work with non-binary options a long as they take on only a few possible values (just code them into more than one bit).
dmckee
A: 

As you already mentioned, one striking point is readability: writeInUpperCaseAndCentered("Foobar!") is much easier to understand than write("Foobar!", true, true), although you could eliminate that problem by using enumerations. On the other hand, having arguments avoids awkward constructions like:

if(foo)
  writeInUpperCaseAndCentered("Foobar!");
else if(bar)
  writeInLowerCaseAndCentered("Foobar!");
else
 ...

In my humble opinion, this is a very strong argument (no pun intended) for the argument way.

Greg S
+2  A: 

I'd go for a combination of methods 1 and 2.

Code a method (A) that has all the arguments you need/can think of right now and a "bare" version (B) with no extra arguments. This version can call the first method with the default values. If your language supports it add default arguments. I'd also recommend that you use meaningful names for your arguments and, where possible, enumerations rather than magic numbers or a series of true/false flags. This will make it far easier to read your code and what values are actually being passed without having to look up the method definition.

This gives you a limited set of methods to maintain and 90% of your usages will be the basic method.

If you need to extend the functionality later add a new method with the new arguments and modify (A) to call this. You might want to modify (B) to call this as well, but it's not necessary.

ChrisF
+1  A: 

I've run into exactly this situation a number of times -- my preference is none of the above, but instead to use a single formatter object. I can supply it with the number of arguments necessary to specify a particular format.

One major advantage of this is that I can create objects that specify logical formats instead of physical formats. This allows, for example, something like:

Format title = {upper_case, centered, bold};
Format body = {lower_case, left, normal};

write(title, "This is the title");
write(body, "This is some plain text");

Decoupling the logical format from the physical format gives you roughly the same kind of capabilities as a style sheet. If you want to change all your titles from italic to bold-face, change your body style from left justified to fully justified, etc., it becomes relatively easy to do that. With your current code, you're likely to end up searching through all your code and examining "by hand" to figure out whether a particular lower-case, left-justified item is body-text that you want to re-format, or a foot-note that you want to leave alone...

Jerry Coffin
The question is tagged "C" which doesn't have objects...
MikeD
The C standard claims that it has objects. The fact that in this case the objects in question are created with the `struct` keyword doesn't change the idea a bit.
Jerry Coffin
@MikeD: To us old procedural programmers "object" has broader application: it can mean a data structure coded into a `struct`. The important thing is that it is a logical unit in the program.
dmckee
A: 

I suggest more cohesive functions as opposed to superfunctions that can do all kinds of things unless a superfunction is really called for (printf would have been quite awkward if it only printed one type at a time). Signature redundancy should generally not be considered redundant code. Technically speaking it is more code, but you should focus more on eliminating logical redundancies in your code. The result is code that's much easier to maintain with very concise, well-defined behavior. Think of this as the ideal when it seems redundant to write/use multiple functions.

A: 

Argument functions, with enums as a variable + a global state if the function benefits from it. I like to OR them together, and viola - a readable function!

#define FRM_UPPERCASE 1<<0
#define FRM_LOWERCASE 1<<1
#define FRM_ALIGN_LEFT 1<<2
#define FRM_ALIGN_RIGHT 1<<3
#define FRM_ALIGN_CENTER 1<<4

#define USE_DEFAULT -1

int write(char* text, int mode = USE_DEFAULT);    
...    
write("foobar", FRM_UPPERCASE|FRM_ALIGN_CENTER);   


setmode(FRM_UPPERCASE|FRM_ALIGN_CENTER);
write("foobar");

You can easily code the parser code which defaults to some values or a global variable. You can also combine the modes to get styles in your case :)

#define FMT_HEAD FMT_CENTER|FMT_UPPERCASE

This is how I define my functions, how win32 exports its functions, and its worked out fine so far

Greg
I somehow missed toraks reply
Greg
There are not default arguments in C
Hernán Eche