The Fallacy of “Design Before Implement”?

Go To Last Post
78 posts / 0 new

Pages

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:
The file needs a name, of course, so I include the naming algorithm in this function.
That increases cyclomatic complexity.

 

ka7ehk wrote:
3) It is SO tempting to include that flag-clear operation in the file data write routine.
So does that.

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

It sounds worse than it was. I should have written "naming function". It is called in a higher level function that includes a call to the "create new file" function and performs several other discrete operations related to opening a new file. This part, specifically, is not so bad on complexity.

 

I just realized that I had a former "personal practice" related to complexity management. I always write an information header for every function I create. What I used to do is list all of the global variables that are touched by the function in that information block. And, I would also list all of the situations that would cause the file to exit at a point earlier than its normal end. Don't know why I got out of that practice, but I need to start doing it again. My mental memory was never particularly good and, now, it is very certainly NOT better. Both of these would help.

 

Jim

 

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

Last Edited: Tue. Apr 2, 2019 - 03:19 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:
It is SO tempting to include that flag-clear operation in the file data write routine.
I don't understand why this is tempting at all? Think of doing this in C++. You will likely have some class that provides the filing functions and some class that derives from it ("wraps it"?) that does things like maintaining this flag. The flag itself would be a private: member of this wrapping class. The filing stuff couldn't even see it! You should try to localise data use as much as possible, private: and protected: in C++ facilitate this.

What I used to do is list all of the global variables that are touched by the function in that information block. 

I know there'll be arguments about efficiency and all that but the idea should be to have no globals. Isolate data to the place where it is used and if it needs to be exposed use getters/setters. Again C++ class members encourage this. 

Last Edited: Tue. Apr 2, 2019 - 08:33 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:
3) It is SO tempting to include that flag-clear operation in the file data write routine. A

That doesn't necessarilty increase Cyclomatic Complexity as mentioned earliar but does violate the "Single Responsiblity Principle" or alternatively Separation of Concerns, the book I mentioned in #44 covers this in depth.

 

ka7ehk wrote:
What I used to do is list all of the global variables that are touched by the function in that information block. And, I would also list all of the situations that would cause the file to exit at a point earlier than its normal end

I honestly don't recommmend doing that because you are in effect writing the function twice plus it's really difficult to keep the header in sync with any modifications you write later. Nothing confuses more than comments that don't match the code.

 

Modern IDE's help enormously in finding where a global variable is directly written. There is no longer the need to document this separately.

 

Last Edited: Tue. Apr 2, 2019 - 02:20 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

N.Winterbottom: The reason that summary helps me is that I do not have to remember what is buried in that function. I also don't have to scan the function every time I use it somewhere new. Its all summarized there at the top. Sure, it is difficult to keep in sync, but it helps me. I am willing to trade physical labor for not having a good memory.

 

clawson: I honestly know almost nothing about C++. Maybe a slight bit about the mechanics but nothing about the philosophy. If I wanted to organize my code as a C++ program, I would have started out that way. But, I didn't. Frankly, I use quite a few global variables. Thats the way its written.

 

Jim

 

 

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:
clawson: I honestly know almost nothing about C++. Maybe a slight bit about the mechanics but nothing about the philosophy. If I wanted to organize my code as a C++ program, I would have started out that way. But, I didn't. Frankly, I use quite a few global variables. Thats the way its written.
You can write modular C. In fact the first C++ was written in C. it's just that C++ casts in concrete concepts that it's all too easy to violate in C.

 

While this is a gross simplification C++ is basically C structs that not only group data but also group together the functions that act on that data (and prevent anything from outside from interfering. Consider a sprite in a game:

typedef struct {
    int xpos;
    int ypos;
    int frame_num;
} sprite_t;

int move_sprite(sprite_t * sp, int newx, int newy) {
    sp->xpos = newx;
    sp->ypos = newy;
}

int set_frame(sprite_t * sp, int frame) {
    sp->frame_num = frame;
}

sprite_t sprite;

int main(void) {
    move_sprite(&sprite, 100, 70);
    set_frame(&sprite, 7);
}

So far so good. I have a sprite and I have some functions that can perform action on its data. Hopefully it's clear I am setting it to appear at (100,70) using frame 7? But see what happens when I do:

int main(void) {
    move_sprite(&sprite, 100, 70);
    set_frame(&sprite, 7);
    sprite.xpos = 37;
}

Something outside the sprite support itself has messed with the data that "belongs" to the sprite by making a direct write to one of the fields within it. In C++ this would have been:

class {
private:
    int xpos;
    int ypos;
    int frame_num;
public:
    int move_sprite(int newx, int newy);
    int set_frame(int frame);
} Sprite;


int Sprite::move_sprite(int newx, int newy) {
    xpos = newx;
    ypos = newy;
}

int Sprite::set_frame(int frame) {
    frame_num = frame;
}

Sprite sprite;

int main(void) {
    sprite.move_sprite(100, 70);
    sprite.set_frame(7);
}

Now any attempt to:

sprite.xpos = 37;

will be met with "you are not allowed to access the private member 'xpos'".

 

This makes the whole sprite thing ("class") a complete, encapsulated package with only access to it being through carefully controlled public functions. You keep everything to do with  sprites all wrapped up in this little package. There's no reason for anything outside the sprite world to know of, or more importantly get access to the data it holds.

 

While no one says you have to learn C++ to aim for modularsied code with protected data, what you might do is at least consider grouping the data for any particular module into struct{}s so you have ADC_struct{}, LCD_struct{} and so on. While (by definition) everything in a stuct{} is "public:" it does at least make you think harder about what data "belongs" to which module. Instead of having some "anonymous" global called "counter" or something which might be a counter for the ADC or a counter for a timer or a counter for a UART you now have ADC.counter, LCD.counter, TIMER.counter and so on which not only make their role more obvious but has the additional benefit of avoiding name pollution. Both the ADC and the TIMER can now both have a thing they call "counter" and so on.

 

Long before I used C++ I worked on projects using C in a very modular way like this. So much so that all the functions of a module would always have a name starting with the module name - so there'd be ADC_init(), UART_init() and so on. In such systems we also tended to do the struct{} grouping of variables so most ADC functions would take as a minimum a (ADC_t * adcvars, ...) as a first parameter so the struct full of vars could be passed around. This is effectively a forerunner to "this" in C++ ;-)

 

And if you don't know C++ then in my example I could have written:

int Sprite::move_sprite(int newx, int newy) {
    this->xpos = newx;
    this->ypos = newy;
}

to make it clearer that "xpos" and "ypos" are members of the class. When C++ functions are called there's a "hidden" first parameter which is a pointer to the collection of variables within the class. In fact not just the variables in the class as a whole but the variables in "this" particular instance of the class.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

While drifting off the original theme of this thread, your code reminded me of something I just ran into and it was something that I thought C provided "protection" through.

 

FatFs provides a typedef'd return value enum

 

typedef enum {
	FR_OK = 0,			/* (0) Succeeded */
	FR_DISK_ERR,			/* (1) A hard error occurred in the low level disk I/O layer */
	FR_INT_ERR,			/* (2) Assertion failed */
	FR_NOT_READY,			/* (3) The physical drive cannot work */
	FR_NO_FILE,			/* (4) Could not find the file */
	FR_NO_PATH,			/* (5) Could not find the path */
	FR_INVALID_NAME,		/* (6) The path name format is invalid */
	FR_DENIED,			/* (7) Access denied due to prohibited access or directory full */
	FR_EXIST,				/* (8) Access denied due to prohibited access */
	FR_INVALID_OBJECT,		/* (9) The file/directory object is invalid */
	FR_WRITE_PROTECTED,		/* (10) The physical drive is write protected */
	FR_INVALID_DRIVE,		/* (11) The logical drive number is invalid */
	FR_NOT_ENABLED,			/* (12) The volume has no work area */
	FR_NO_FILESYSTEM,		/* (13) There is no valid FAT volume */
	FR_MKFS_ABORTED,		/* (14) The f_mkfs() aborted due to any parameter error */
	FR_TIMEOUT,			/* (15) Could not get a grant to access the volume within defined period */
	FR_LOCKED,			/* (16) The operation is rejected according to the file sharing policy */
	FR_NOT_ENOUGH_CORE,		/* (17) LFN working buffer could not be allocated */
	FR_TOO_MANY_OPEN_FILES,	        /* (18) Number of open files > _FS_SHARE */
	FR_INVALID_PARAMETER,	        /* (19) Given parameter is invalid */
	FR_DISK_FULL,			/* (20) +JDW Disk is full */
	FR_BUFFER_OVERFLOW		/* (21) +JDW accel bfr overflow during f_write() */
} FRESULT;

I have a function that is declared, thusly:

 

FRESULT DoDataWrite( void );

DoDataWrite returns the FRESULT return value from f_write() and other such calls within that function. I also have a variable defined this way:

 

 uint16_t MasterEvents;

At one point, thinking that I had changed the internal structure of DoDataWrite() and its return value, I wrote

 

MasterEvents = DoDataWrite();

gcc did NOT catch that as an error and it took forever (metaphorically) to figure out that error and correct it. Yes, I know that if I had organized things "better", it would not have happened, and so forth. But, the question on the table, right here, is: why? Am I incorrect that the typdef (in this case, FRESULT) is supposed to protect variables with that typedef from being incorrectly used with other variables that do not share the same typedef? And, what, other than restructuring the program, would provide a higher level of protection? Typedef everything?

 

Thanks

Jim

 

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

Last Edited: Tue. Apr 2, 2019 - 04:14 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:
gcc did NOT catch that as an error
That alone is an argument for using C++ rather than C. Forget everything else C++ might offer and the one thing you will benefit from is stronger type checking than C.

 

I was trying to remember one of Bjarne Stroustrup's famous quotes about C/C++. Google reminds me that the phrase is actually:

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.

It's the first bit of that which is most pertinent here.

 

BTW if sticking with C I'm not sure if -Wall and -Wpedantic might have helped with the int=enum thing ?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

So, even typedef conflicts are not enforced very rigorously in C?

 

Jim

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Actually, I'm wrong. It's the other way round. C++ will not allow:

enum {
    RED,
    GREEN,
    BLUE
} lights;

lights foo;
int n;

int main(void) {
    foo = 2; // throws "invalid conversion from int to 'lights'"
    n = foo; // allowed because of implict conversion of enum to int
}

So an enum will just be implicitly converted to int when assigned to an int type lvalue.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

Back on the original theme, perusing the text recommended above: "Clean Code - A Handbook of Agile Software Craftsmanship" begins with this momentous statement:

 

The only valid measurement of code quality: WTFs/minute

Jim 

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Did you reach this paragraph....

Learning to write clean code is hard work. It requires more than just the knowledge of principles and patterns. You must sweat over it. You must practice it yourself, and watch yourself fail. You must watch others practice it and fail. You must see them stumble and retrace their steps. You must see them agonize over decisions and see the price they pay for making those decisions the wrong way.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Yes, indeed! I got there. Been doing a whole bunch of "price paying"!

 

Actually, the problem, such as it is, is much larger than what has been discussed above. In some off-thread emails with one of the other contributors, I realized, for the first time that what this really involves is the compression of "proof of concept", "design", "prototyping", and "manufacturing prep" into a single effort. That has really made design confusing and prototyping (especially of code) very sub-optimal. 

 

In my own defense, I THOUGHT that I had the proof of concept already done: this is "just" a software mod for an existing product. I did not recognize that code is part of proof of concept. Actually, when I started this effort, I THOUGHT that design was done, also. Again, it was "just" supposed to be a software mod for an existing product. When I started, I simply did not recognize that the structure of the code for that existing product was so rigid that I could not make the mod without what became an almost total rewrite (there was a command parser that I could still use). 

 

So, there were lots of mistakes and lots of underestimates. The consequences of those are where I am right now. And, where I am right now is that I cannot afford to do another total rewrite. I have customers waiting. 

 

Jim

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

Last Edited: Tue. Apr 2, 2019 - 04:52 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

"Dare to be naïve." - Buckminster Fuller

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Good suggestion!

 

Thanks

Jim

Jim Wagner Oregon Research Electronics, Consulting Div. Tangent, OR, USA http://www.orelectronics.net

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

"Dare to be naïve." - Buckminster Fuller

Last Edited: Tue. Apr 2, 2019 - 06:04 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

 I THOUGHT that design was done

I've had to deal with that trap.  Usually as soon as something starts to look like it is working, too many others think it is "done" & question why we are still working on it.  In a larger frame of reference...there is always progress, sometime fast, sometimes slow.  Products that stop developing/evolving simply die off.

Of course, having a working proto is just one small step. 

 

I have customers waiting. 

Three years from now most won't care, remember, or even know about that, they will only care that things work properly & well.   When I buy a TV, if the picture is lousy, I don't care that they "had to ship it next week", since that might have been 2 years before I purchased it.  Of course, there must be a limit to ship some version of the product...if it took another 5 years for version 1, I wouldn't have the TV.

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Once I worked on a product that was originally

prototyped in a scripting language.  We got it

working in about two months.  All we had to do

was rewrite this prototype in C++ and then the

money would start rolling in.

 

Our estimate was two more months to complete

the rewrite.  Obviously it wouldn't take longer

than that since we already worked out all the

issues and the prototype was doing what it was

supposed to.  The reality is we didn't finally ship

the product until two years later.

 

This is normal.

 

--Mike

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

 Ohhh, how sad. I had missed several of these conditional cases, and the algorithms that the functions implemented were really incorrect. For all practical purposes, both now need to be rewritten. But, even now, have I identified ALL of the conditional cases? I am not sure! I think I have, but I am not certain and I fear that I won’t know until testing after the rewrite of the rewrite happens.

Many times, two approaches on paper might look similar, but produce vastly different results.   Describing the logic from different perspectives may simplify the programming or make it easier to see the corner cases.  Sort of like walking to the store can be described using rectangular or polar coordinates---one may be vastly simpler, though they both get you to the store.  Perhaps minimizing interdependencies is a worthy goal.  Working top-down vs bottom up---does one method typically produce fewer side-effects?

There is not much substitution for the product learning curve.

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

And even when you think you've designed something properly things can still bite you...

 

I have for a customer several hundred units out in the field that comprise a keypad and LCD display unit, and a 'brains' unit. Press a key and a keycode is sent to the brains over a full-duplex serial link and when the brains needs to tell the operator something it sends an ASCII string back which gets put, almost as is, onto the LCD display. Simple.

 

I now want to retrofit a unit halfway down the line which can inject keycodes for the brain, which is easy and interpret the messages that the brain sends back. And that's turning out to be quite a challenge.

 

The problem is that because I was simply displaying messages I just sent a CLS (clear screen) to the LCD and then wrote to it a variable length text string. And when I'd sent a complete message I just stopped sending ASCII. So now when I want to interpret those messages I have no way of knowing when a complete message has been received. I can sync to the start of message by looking for the CLS, I can store the message in a buffer ready to parse, but that's it. There's nothing come down the line to say "what has come before me is the complete message, go ahead and parse."

 

The displayed messages are all variable length so I can't trigger when a byte count has been reached. There's nothing particularly unique at the end of the line. Nothing.

 

The fool-proof way to solve this is to maintain a virtual LCD display in a buffer and to constantly check it to see if the message is complete but the code to do that is just horrible. My backstop is to do it by using a timeout. Start a timer on receipt of CLS, wait a period of time longer than the slowest message and then trigger the parse routine. Luckily this doesn't need real-time results.

 

All for the lack of a CR/LF at the end of a display string.

#1 This forum helps those that help themselves

#2 All grounds are not created equal

#3 How have you proved that your chip is running at xxMHz?

#4 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand." - Heater's ex-boss

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If it is CLS-"hello"-CLS-"world"-CLS-"etc" then isn't the CLS itself the delimiter ? (ie not only telling you that a new message is starting but the last has now finished.

 

Or is this actually like:

 

CLS-"hello"-<23 minutes>-CLS-"world"-<2 days>-CLS-"etc"

 

so you cannot wait for the "next" CLS?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I get the impression that Brian is not allowed to change the brain.

Otherwise, I'd expect the problem to be trivial.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

Or is this actually like:

CLS-"hello"-<23 minutes>-CLS-"world"-<2 days>-CLS-"etc"

so you cannot wait for the "next" CLS?

 

Yep, that.

 

It's...

[push buttons] -> [CLS+confirmation message] -> [something happens over an indeterminate time period] -> [CLS + updated status] -> [time passes] -> repeat. And it's the updated status that I need.

#1 This forum helps those that help themselves

#2 All grounds are not created equal

#3 How have you proved that your chip is running at xxMHz?

#4 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand." - Heater's ex-boss

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

skeeve wrote:

I get the impression that Brian is not allowed to change the brain.

Otherwise, I'd expect the problem to be trivial.

 

I'd really rather not. I'm looking at the code for it now though.

#1 This forum helps those that help themselves

#2 All grounds are not created equal

#3 How have you proved that your chip is running at xxMHz?

#4 "If you think you need floating point to solve the problem then you don't understand the problem. If you really do need floating point then you have a problem you do not understand." - Heater's ex-boss

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

All for the lack of a CR/LF at the end of a display string.

While you met the "requirements" of the original project, sometimes that is not enough.  Part of the  design should "look ahead"  ...what might be reasonably needed in the future?  Of course, having a crystal ball really helps.  That's probably why most every design eventually evolves a bit, over a long time frame.  The app in 5 years may look only like a cousin of the original.

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'd expect fairly trivial changes to brain would be sufficient.

E.g. start each message with plus-backspace and end them with period-backspace.

A less trivial change would be to do that only for messages that need interpreting.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

or change the perspective ... consider a Turing Machine ... consider how data (AVR Freaks operators and their handles) can be transformed into information (names, some partial names, some complete names by one's memory of earlier posts)

Turing Machines (Stanford Encyclopedia of Philosophy)

...

 

1.1 Turing’s Definition

https://plato.stanford.edu/entries/turing-machine/#DefiTuriMach

...

It is supplied with a one-way infinite and one-dimensional tape divided into squares each capable of carrying exactly one symbol. 

...

The machine is an automatic machine (a-machine) which means that at any given moment, the behavior of the machine is completely determined by the current state and symbol (called the configuration) being scanned. 

...

The end of any sequence is by the terminating state (pattern recognized, onto the next pattern) (an example : IIRC, there's the C obfuscation challenges)

 

You'll get your fill of Turing Machines in a course on formal methods.

 

"Dare to be naïve." - Buckminster Fuller

Pages