modularity... (was: Re: Looking for real world examples to explain the difference between procedural (structured?) programming and OOP)



[decided to move this to the toplevel mainly as this has suddenly shifted to
essentially a new topic]


"H. S. Lahman" <h.lahman@xxxxxxxxxxx> wrote in message
news:nSdSl.313$Cc1.194@xxxxxxxxxxxxxxxxxxxxxxx
Responding to cr88192...

I mean more like:
typedef struct FooContext_s FooContext;
struct FooContext_s {
int a;
float *b;
...
};

int FooFuncA(FooContext *ctx, float a, float b);
int FooFuncB(FooContext *ctx, int v);
int FooFuncC(FooContext *ctx, char *name);
...

From an OO perspective all you are doing is defining the message data
packet as an object. But...

in this case, the context serves a similar role to an object instance in
an
OO language.
a context may also serve as a way to eliminate using globals in some
cases
as well...

That's fine in functional programming but it doesn't play well in OO
development because...

as I see it, arguments are not part of the general context of a function
(nor especially part of a context struct), but are more specific (they
only
apply to the current invokation, and not to any side-state).

In a well-formed OO application there is rarely a data packet in
messages. That's because it creates unnecessary dependencies between
caller and callee (i.e., the caller needs to know what data the callee
needs). [The only time you see data packets is when requirements demand
some sort of synchronization, such as processing all the sensor readings
from the same time slice from multiple sensors.]


I think the point was misunderstood.

FooContext would be used as a means of holding the state, and typically all
the functions in a given module will share the same context (they pass it
around, ...). this is in contrast to using globals to store this
information, since it allows that multiple contexts may exist (and thus, be
able to have the same module manage more than one task at a time...).

this is similar to how 'this' is passed in the implementation of many OO
languages (go and dig around in many C++ or in the JVM or MSIL, and one will
find that 'this' is usually passed as a sort of invisible argument...).

for my uses, I usually pass it explicitly, both because this maps most
nicely to plain C, and also because arguments are faster than some other
options, such as thread-local-variables, ...


Corollary: OO behavior methods in a well-formed OO application never
return a value because that would create an instant hierarchical
dependency for the caller specification on what the callee does and how
it does it.


typically, for functions returning int, the int is typically either ignored,
or used as a means to return a status code (usually -1=error, 0=ok, other
positive numbers usually mean things are ok, but some specific situation may
need to be addressed, and other negative numbers are usually used to
indicated specific error conditions, vs -1 which is generic...). by
convention, one normally just returns 0 if they have nothing better to
return.

(there is a loosely similar set of conventions for functions which return
pointers, such that a pointer can be used to either successfully return a
value, or return a status code, where NULL is traditionally the most common
status code, but in my uses sometimes others are used as well...).

another convention used by many APIs, is to provide some sort of 'errno'
like variable, at which point only a generic status is returned by functions
(0 or -1), and this variable is polled to get a more specific error...

but, alas, this is C tradition of some sort, I had not thought much of it...


I guess in Java and C# and friends, people like throwing exceptions...

the problem in C is that there is no standard/good way to do exception
handling, or at least which is portable. I have some custom library
functions which provide for exception handling in C, but they hack into
WIN32's SEH system (normally... SEH assumes MSVC which provides __try and
__catch and similar... but it is less clean if one's compiler doesn't
natively support this...).

other common approaches for exception handling in C revolve around using
longjmp (and typically also TLS or globals...).

so, FWIW, status codes remain the most common in practice...

i=FooFunc(ctx);
if(i<0)
... //oh crap, FooFunc failed for whatever reason...


if a multi-part process needs to actually return a value of some sort, I
usually delay it until the end.

fooBegin(ctx, FOO_SOMETHING);
....
stuff for sending in parameters and/or building sub-objects...
....
hdl=fooEnd(ctx);

this way, whatever is done/built between begin and end, and at 'end' a
handle can be returned...


this does not create nearly the sorts of dependency issues that seem implied
here, as a return value is a return value, not some kind of contract for a
specific algo...


The OO paradigm depends on relationships to constrain access to data and
wants to make sure only the entities that need the data access it. When
relationships are instantiated at the object level they constrain what
objects, and consequently what data, are accessible. The OO paradigm
goes to substantial lengths to separate issues around Who participates
in collaborations from When collaborations occur and What happens when
they do. Consequently we have things like "factory" objects that isolate
the rules and policies of relationship instantiation and a third of the
GoF design patterns are about object and relationship instantiation.


I am not sure I understand entirely what is meant by this...


But that only works well if the object that needs data navigates a
relationship path to get it. So methods are expected to go get any data
they need. When some other object decides what data is needed, one is
creating a needless dependency. Worse, it means that other object must
understand the implementation of the object needing the data, which
completely trashes encapsulation (aka modularity). IOW, knowledge access
is always peer-to-peer rather than through middlemen.


context structs are usually used for this purpose...


the only other alternative is to not use contexts, and then be bound by
whatever is the global state.
many APIs don't need a passed context though, as the entire API behaves like
a stateful module (contexts are then usually used internally within the
"walls" of the API).

other times, things (including contexts) may be passed outside of a
module/API, but then usually take on a role as abstract handles (may often
be cast to a 'void *' or similar, or sometimes represented externally as an
integer), and it is assumed that for any manipulations or information
requests, the handle is passed back to the API and into any relevant
functions...

we have no idea what happens in those functions, or even what exactly the
handle points to, only that the API functions are expected to do certain
things (and may, in turn, return new handles, or maybe status codes...).


<aside>
Note that in OOA/D this is so important that knowledge and behavior
collaborations are treated very differently. Behavior access is
inherently asynchronous because of the separation of message and method
and one needs to construct the model with that in mind (e.g., provide
things like handshaking synchronization). But in an OOA/D model it is
assumed that knowledge access is aways synchronous. So if the actual
implementation is distributed, one will have to provide suitable
infrastructure (blocking, etc.) for data integrity to ensure that data
access works as-if the method had direct, immediate access.

This allows one to resolve customer functional requirements without
knowing what type of specific computing environment one will actually
have. So issues like concurrency and distributed processing are never
relevant for OOA solutions and are rarely relevant for OOD models. That
is only possible if knowledge access is synchronous while behavior
access is asynchronous.


sometimes it is useful to assume that both are async...
so, data may then be known locally, but may be allowed to be "stale".

other times, data is usually directly queried from some other location, so a
specific API will manage a certain piece or type of data, and any requests
for this data will go to this location...

an few examples would be in my case, the VFS, dynamic linker, and metadata
manager, where each manages a specific set of things, and is essentially the
authority for whatever it is that it manages (virtual files, the layout of
the running program image, the structural/semantic aspects of currently
resident code, ...).

hmm, "don't go thunking without the assembler...".


so:
sync data, sync flow: may be useful in controlled situations (specific
regions of code, or small apps);
async data, sync flow: may be useful in other situations;
sync data, async flow: typical of modular systems;
async data, async flow: sometimes useful (for example, modules may operate
autonomously with only approximate data, often using indirect means to
communicate or synchronize).


That comes full circle to this issue because data integrity in the
implementation is much easier to manage in a concurrent/distributed
environment if state data access is handled separately from behavior
access on an as-needed basis. IOW, the scope of data integrity only
includes the method needing the state data and the object owning the
data because there are no long chains of methods passing state data to
the ultimate user, allowing arbitrary delays that could affect the
timeliness of the data.


errm.... to me this just sounds bizarre...


like users are sitting there directly invoking functions and actually having
a relevant perception of all that goes on between function call and
return...

then again, it could be that I usually write code which operates in
"soft-real-time", and so any complex operations are usually designed such
that they can be split into bunches of little pieces and performed over some
number of frames (trying to avoid any long operation which could potentially
risk hurting the framerate...). (a process needs to either be able to be
split over successive frames, or fast enough that it can be done every
frame, or if not, then it may not be reasonable to perform... if reasonable,
side processes may often be split off into separate threads, such that
certain delay issues are not as severe...).

this, again, comes back to contexts. a context is very often a convinient
way to "slice and dice" control flow so that it can be performed
incrementally (well, along with CPS, but CPS is a PITA in C...).

so, the context struct holds most of the state, and the functions primarily
work as finite-state-machines, and so I can just call some function to
"work" as much as needed (usually, this function is called in a loop), then
stop when whatever condition is met (out of time, or all that is needed to
be done is done for now, ...), and activity may be resumed on the next
frame.

the kind of control flow issues you are describing (super-long winding and
highly interdependent processes), just don't seem to be much of an issue in
my case...


That's why if you want to write elegantly terse programs quickly,
functional programming definitely is the way to go, especially in
scientific arenas. But if requirements change it is usually easier to
rewrite them than to modify them because of those long, hierarchical
dependency chains where state data is passed up and down the call stack.
So if requirements are volatile over the life of the application one
prefers the OO view of modularity, despite it being more verbose and
less intuitive vis a vis the hardware computational models. That's
because of the militant minimization of dependencies through notions
like encapsulation and peer-to-peer collaboration. In fact, one can make
a good case that the main contribution of the OO paradigm is the
elimination of hierarchical behavior dependencies.
</aside>


as noted above...
these heirarchical issues are not usually too much of a problem.

but, yes, I guess it could be argued that I usually use a "functional" style
on the small-scale, and switch to a modular/"OO" style for everything much
larger.

but, the issue is, not everything is the same.

sometimes, stateless functions and atomic data elements are the way to go
(usually for performing specific tasks), and then one switches to
alternative strategies for the larger scale...


it is not like everyone is sitting around writing batch systems...
and it is not like someone needs something like Java or C# to write anything
that is not a batch system (or, OTOH, that because something is written in
Java it would not be a batch system...).



--
Life is the only flaw in an otherwise perfect nonexistence
-- Schopenhauer

H. S. Lahman
H.lahman@xxxxxxxxxxx
software blog: http://pathfinderpeople.blogs.com/hslahman/index.html


.



Relevant Pages

  • Re: End-of-the-week fun
    ... > to declaring it as 'signed int'. ... there is a context in both C ... > struct S { ... The first expression has type pointer-to-member, ...
    (comp.lang.cpp)
  • [SCSI] fix wrong context bugs in SCSI
    ... This is the second half of the execute_process_contextAPI addition: ... fix wrong context bugs in SCSI ... Also clean up the scsi_target structure releasing via this API. ... struct scsi_device *sdev; ...
    (Linux-Kernel)
  • [PATCH] Make common helpers for seq_files that work with list_head-s
    ... Many places in kernel use seq_file API to iterate over a regular ... int n = 0; ... #ifdef CONFIG_PROC_FS ... list_entry(v, struct afs_vlocation, link); ...
    (Linux-Kernel)
  • Re: How to read the audio file data
    ... What is the API which helps to read the data of an audio file. ... the struct, and so can be read from the struct, and the end of the struct ... int fsz; //filesize ...
    (comp.compression)
  • Re: Threads on dual core dual processor
    ... The term 'object' is not really a precise term in this context, ... This is for the same reason 'int a;' only creates a single object ... It defines a struct, instantiation of which creates at least 11 objects ...
    (comp.programming.threads)