Pshew... this might be a long answer... but here goes...
First, let's start with this statement:
Maybe this doesn't even make sense in C because of the lack of formal OOP language constructs?
Couldn't disagree more with that statement. As I'll show later on; just because C doesn't have nifty keywords like "class" doesn't mean you can't accomplish the same things.
I'll try to go through this step-by-step as best I can - following your question's flow.
OOP in C
I suspect, based on the phrasing of your question, that you have a pretty decent grasp of OOP concepts (you're even thinking in terms of patterns and even have a good idea of how those patterns will play out for your particular scenario) - so let me do an "OOP in C" tutorial in "30 seconds or less".
Once you get the hang of things you'll realize there is a lot more you can do than what I'm going to show here - but I just want to give you a taste.
101
First, we'll start with a basic "class" (go with me on this):
Foo.h:
typedef struct Foo Foo;
Foo * FooCreate(int age, int something);
void FooSetAge(Foo * this, int age);
void FooFree(Foo * this);
Foo_Internal.h: (you'll see why I broke this out in a second)
#include "Foo.h"
struct Foo {
int age;
int something;
};
void FooInitialize(Foo * this, int age, int something);
Foo.c:
#include "Foo_Internal.h"
// Constructor:
Foo * FooCreate(int age, int something) {
Foo * newFoo = malloc(sizeof(Foo));
FooInitialize(newFoo);
return newFoo;
}
void FooInitialize(Foo * this, int age, int something)
{
this->age = age;
this->something = something;
}
// "Property" setter:
void FooSetAge(Foo * this, int age) {
this->age = age;
}
void FooFree(Foo * this) {
// Do any other freeing required here.
free(this);
}
Couple things to notice:
- We hid the implementation details of
Foo
behind an opaque pointer. Other people don't know what is in a Foo
because that implementation detail is in the "internal" header file, not the "public" header.
- We implement "instance methods" just like an OOP language would - except we have to manually pass the "this" pointer - other languages just do this for you - but it's not a big deal.
- We have "properties". Again, other languages will wrap up property getters/settings in a nicer syntax - but all they are really doing behind the scenes is creating some getter/setter method for you and translating calls to the "properties" into method calls.
Inheritance
So what if we want a "subclass" of Foo
- which only adds additional functionality - but can be substituted for a Foo
? Simple:
FooSubclass.h:
typedef struct FooSubclass FooSubclass;
FooSubclass * FooSubclassCreate(int age, int something, int somethingElse);
void FooSubclassSetSomethingElse(FooSubclass * this, int somethingElse);
void FooSubclassFree(FooSubclass * this);
FooSubclass_Internal.h:
#include "FooSubclass.h"
#include "Foo_Internal.h"
struct FooSubclass {
Foo base;
int something;
};
void FooSubclassInitialize(FooSubclass * this, int age, int something, int somethingElse);
FooSubclass.c
#include "FooSubclass_Internal.h"
// Constructor:
Foo * FooSubclassCreate(int age, int something, int somethingElse) {
FooSubclass * newFooSubclass = malloc(sizeof(FooSubclass));
FooSubclassInitialize(newFooSubclass, age, something, somethingElse);
return newFooSubclass;
}
void FooSubclassInitialize(FooSubclass * this, int age, int something, int somethingElse) {
FooInitialize(this, age, something);
this->somethingElse = somethingElse;
}
void FooSubclassSetSomethingElse(Foo * this, int somethingElse)
{
this->somethingElse = somethingElse;
}
void FooSubclassFree(FooSubclass * this) {
// Do any other freeing required here.
free(this);
}
Now, I should mention, just like we made "initializers" which don't actually call malloc
, but are responsible for initializing the member variables - we also really need deallocators - which don't actually free the struct - but instead free/release any "owning" references, etc. However... I'm actually going to mention something in the section below which might explain why I didn't bother with that yet.
You should notice now - that since our FooSubclass
's first member is, in fact, a Foo
struct - that any reference to a FooSubclass
is also a valid reference to a Foo
- meaning it can be used as such pretty much anywhere.
However, there are a few small issues with this - like I mentioned in the paragraph before last - this technique doesn't actually let you change behavior of the base class. (Something we'd like to do for deallocating our instance, for example).
Polymorphism
Let's say we have some method - we'll come up with a random BS example - called calculate
.
We want calling calculate
on a Foo
to return one value - but a different value if it was called on a FooSubclass
.
This is simple in C - it's really just a matter of creating a wrapper method which actually calls a function referenced by a function pointer. OOP languages do this for you behind the scenes and it's usually implemented via what's referred to as a VTable.
Here's an example (I'm going to stop giving complete examples and instead focus on the relevant parts):
First we define the signature of the method. Here we're saying "calculateMethod" is: a pointer to a method which takes one parameter (a pointer) and returns an int.
typedef int (*calculateMethod)(void *);
Next, we add a member variable in our base class which will point to some function:
struct Foo {
// ...
calculateMethod calc;
// ...
}
We initialize this with some initial value in the FooInitialize
method (for our base implementation):
int FooCalculate(Foo * this)
{
this->calc(this);
}
int FooCalculateImplementation(void * this)
{
Foo * thisFoo = (Foo *)this;
return thisFoo->age + thisFoo->something;
}
void FooInitialize(Foo * this, ...)
{
// ...
this->calc = &FooCalculateImplementation;
// ...
}
Now we make some way for subclasses to override this method - say, for example, a method declared in the Foo_Internal.h
file called void FooSetCalculateMethod(Foo * this, calculateMethod value);
- and voila! Methods which can be overridden in subclasses.
Model
Our model would typically consist of data acquisition from Analog to Digital converters in the product.
OK - so, Model is probably the easiest thing to implement - simple "classes" which are used as data storage mechanisms.
You'll have to figure something out for your particular scenario (being an embedded system I'm not sure what your exact restrictions will be - and if you're worried about RAM / persistence / etc) - but I think you don't want me to dive into that anyways.
View
The views might be a web page powered by an embedded web server, or else an LCD screen with capacitive touch control.
For physical things your "view" may be fixed buttons on a control panel - or, like you said, it could be an LCD or HTML.
The bottom line here is you simply need classes which are capable of presenting the rest of your system with a "simple" interface for displaying/changing things in the view - and encapsulate the details of IO to the user.
Typically the "I" part of "IO" needs at least some small wedge of code in the view.
I don't think this is ideal - but, most of the time, there isn't a good way around having your "view" proxy user input back to your controllers. Maybe with your system there is a good way around this - given you have total control.
I hope you can see now how you could easily go about creating some view classes which are relevant to your needs.
Controller
Our controllers would more or less be the glue logic that manages the relationship between these two areas of code.
This is usually the guts of the application. You'll likely need more than one controller around at a given time - one for ingress/processing of sensor data, one or more for whatever UI you've got active, and possibly others.
Anyways, I hope that helps... I feel like I'm writing a book now, so I'll stop.
Let me know if you want more, or if that helps at all.