私はファームウェア開発者であり、特にSOLIDマイクロコントローラーのハードウェアアブストラクションレイヤーで低レベルプログラミングのARMプラクティスを適用することに興味があります。
私がインターネットで遭遇するすべての例は、C++またはC#またはJavaで実装されており、Cでこれらのパターンに従うのは少し難しいようです。
Cでそれを機能させる方法のヒントを教えてくれる例はありますか?
SOLIDを適用することは常に適切であるとは限りません。依存関係の逆転はある程度の間接化を意味し、通常はオーバーヘッドを意味します。この種のオーバーヘッドはメモリに制約のあるデバイスでは適切ではない可能性があります。しかし、すべてが失われるわけではありません。関連するOOP Cの機能を実装できますが、プリプロセッサを使用すると十分な柔軟性が得られることもあります。
典型的な依存関係の逆転の例は、この種のコードをリファクタリングします:
class Dependency {
int concreteStuff;
}
class Context {
Dependency d;
void doSomething() {
print(d.concreteStuff);
}
}
new Context(new Dependency()).doSomething();
に:
interface Interface {
int getConcreteStuff();
}
class Dependency implements Interface {
int concreteStuff;
int getConcreteStuff() { return this.concreteStuff; }
}
class Context {
Interface i;
void doSomething() {
print(i.getConcreteStuff());
}
}
new Context(new Dependency()).doSomething();
CにはJavaの意味でのインターフェースはありませんが、このOOPのような機能(ランタイムポリモーフィズム)を自分で実装することは1つのオプションです。
// interface:
typedef struct {
void* data;
int (*getConcreteStuff)(Interface*);
} Interface;
// dependency:
typedef struct {
int concreteStuff;
} Dependency;
static int getConcreteStuff(Interface* interface) {
return ((Dependency*)interface->data)->concreteStuff;
}
Interface Dependency_new() {
Dependency* d = malloc(sizeof(*d));
d->concreteStuff = 0;
return { d, getConcreteStuff };
}
// context:
typedef struct {
Interface i;
} Context;
void Context_doSomething(Context* ctx) {
printf("%d\n", ctx->i.getConcreteStuff(&ctx->i));
}
// composition
Context ctx = { Dependency_new() };
Context_doSomething(&ctx);
Interface
は、インターフェイスメソッドへの関数ポインターを格納する従来のvtableを表します。関数ポインターが数個しかない単純なケースでは、明示的なインターフェイスを取り除き、ポインターをコンテキストに直接格納できます。コンテキストは具体的な依存関係について何も認識せず、インターフェース関数ポインターを介してのみ対話します。実際の依存関係はvoidポインターの背後に隠されています。すべての場合において、具体的な依存関係は構成中に解決され、実行時に自由に選択できます。
したがって、実行時にさまざまな依存関係を選択する機能が必要な場合、または考えられるすべてのインターフェース実装がわからない場合(たとえば、他のアプリケーションによって拡張されるライブラリを作成している場合)は、この種のアプローチが適しています。
しかし、そのような実行時の柔軟性は必ずしも必要ではありません!特に組み込みのコンテキストでは、ビルド時に依存関係を解決してから、適切な構成をフラッシュできる場合があります。また、可能性のあるすべての依存関係を事前に知っている可能性もあります。次に、最もC風のアプローチは、プリプロセッサを使用することです。
たとえば、プリプロセッサを使用して、構造体と関数の正しい定義を選択できます。
#ifdef DEPENDENCY = "TEST"
typedef struct {} Dependency;
int getConcreteStuff(Dependency*) { return 42; }
#else
typedef struct {
int concreteStuff;
} Dependency;
int getConcreteStuff(Dependency* d) { return d->concreteStuff; }
#endif
typedef struct {
Dependency d;
} Context;
void doSomething(Context* ctx) {
printf("%d\n", getConcreteStuff(&ctx->d));
}
または、すべての依存関係をコンパイルし、プリプロセッサを使用して正しい依存関係に名前を付けることもできます。
// invoke compiler with -DDependency=TestDependency to use this implementation
typedef struct {} TestDependency;
int TestDependency_getConcreteStuff(TestDependency*) {
return 42;
}
typedef struct {
int concreteStuff;
} StandardDependency;
int StandardDependency_getConcreteStuff(StandardDependency* d) {
return d->concreteStuff;
}
// default to StandardDependency
#ifndef Dependency
#define Dependency StandardDependency
#endif
// helper to call functions with correct name
#define METHOD(m) Dependency ## _ ## m;
typedef struct {
Dependency d;
} Context;
void doSomething(Context* ctx) {
printf("%d\n", METHOD(getConcreteStuff)(&ctx->d));
}
すべてのコードがまだコンパイルされ、型チェックされているため、ビットロートから保護するため、この後者のアプローチを好みます。依存関係関数がインラインであるか、内部リンケージがあるか、またはリンク時最適化を使用することにより、余分に生成されたマシンコードを最適化してスペースを節約できます。