オブジェクト指向設計の考え方と Linux カーネルにおけるその具体化について語る

オブジェクト指向プログラミング (OOP) は、設計思想またはアーキテクチャ スタイルです。OO 言語の父、Smalltalk の発明者であるアラン ケイは、OOP について次のように述べています。

OOP はネットワーク構造を具体化する必要があり、この構造上の各ノード「オブジェクト」は「メッセージ」を通じてのみ他のノードと通信できます。各ノードには内部の非表示状態があり、状態を直接変更することはできませんが、メッセージ パッシングを通じて間接的に変更する必要があります。

この一節を冒頭として、この記事の議論を始めてください。

1. オブジェクト指向の本質

1.1 オブジェクト指向プログラミングはどのような問題を解決しますか?

ほとんどのプログラマーは C 言語を学習していますが、特に組み込みエンジニアは C 言語しか知らない場合があります。

C言語は典型的なプロセス指向言語であり、すべてがプロセスです。単純なシングルチップ プログラムには数行しか含まれない場合があり、多くは数百行以下です。この時点では、人間の能力によってコード全体を完全に実行することができますが、オペレーティング システムが導入されると、事態は複雑になります。

プロセスのスケジューリングやメモリ管理などのさまざまな機能により、コード量は大幅に増加しており、単純な RTOS リアルタイム オペレーティング システムでもコード行数は数万行に達しています。現時点では、それを実行することは不可能ではありませんが、より困難。

しかし、Linux のコード量は常人が読める量ではなく、1 行を 1 秒で読み、1 日 12 時間読むと数年かかる Linux のソースコードは、ただ読んでいるだけで、理解しているわけではありません。

別の簡単な例

小規模な会社では従業員が数名しかいないことが多く、全員が協力して働き、全員が何をしているのかを正確に把握できます。

大企業ではどうでしょうか?何千人、何万人もの従業員がいる会社では、全員が何をしているのかを把握することはまったく不可能です。部門があり、それぞれの部門が責任を分けてそれぞれの仕事を行っているので、一人の人の仕事内容は詳しくは分からないかもしれませんが、営業部門は販売を担当し、研究開発部門は研究を担当し、開発は生産部門が担当し、生産は生産部門が担当します。

オブジェクト指向プログラミングによって解決される基本的な問題は、大規模で複雑なプログラムの構築を容易にするために、プログラムを体系的に編成することです。

1.2 プログラミング言語とオブジェクト指向プログラミングの関係

多くの人はプログラミング言語をオブジェクト指向と結びつける傾向があります。たとえば、C 言語はプロセス指向言語であり、C++ はオブジェクト指向言語です。これは完全に正確ではありません。

オブジェクト指向は一種の設計思想であり、オブジェクト指向はC言語でも完全に実現でき、C++やその他の言語で書かれたプログラムもオブジェクト指向ではなくプロセス指向になる場合があります

C 言語によるオブジェクト指向の実装の最も明白な例の 1 つは Linux カーネルです。Linux カーネルは、複数の比較的独立したコンポーネント (プロセス スケジューリング、メモリ管理、ファイル システム) を完全に具体化するオブジェクト指向の設計思想を完全に採用していると言えます。 ..) コラボレーションのアイデア。Linux カーネルは C 言語で書かれていますが、いわゆる OOP 言語で書かれた多くのプログラムよりも OOP 度が高くなります。

ここでは、C++ を使用して、オブジェクト指向言語でもプロセス指向プログラムを作成できる理由を説明します。

この段落は、C++ の基礎をある程度持っている友人に適しています。C++ を知らない場合は飛ばしても問題ありません。

たとえば、合計価格を計算するプログラムは、数値 * 単価に過ぎません。

#include<iostream>
using namespace std;
class calculate{
public:
	double price;
	double num;
	double result(void){
		return price*num;
	}	
};
int main ()
{
	calculate a;
	a.price=1;
	a.num=2;
	cout<<a.result()<<endl;
	return 0;
}

ダブル11、20%オフという機能を追加しますが、どのように書きますか?

#include<iostream>
using namespace std;
class calculate{
public:
	double price;
	double num;
	int date;
	double result(void){
		if(date==11)
			return price*num*0.8;
		else
			return price*num;
	}	
};
int main ()
{
	calculate a;
	a.price=1;
	a.num=2;
	cout<<"please input the date:"<<endl;
	cin>>a.date;
	cout<<a.result()<<endl;
	return 0;
}

こう書くと典型的なプロセス重視の考え方ですが、なぜでしょうか?ダブル 12 でさらに 30% 割引がある場合、この考えに従ってどのように記述しますか? 次に、計算クラスに if else を追加して、旧正月に 50% 割引がある場合はどうなるかを判断します。次に、計算クラスに if else を追加して判断します。オブジェクト指向の考え方を記述する方法を見てみましょう。

#include<iostream>
using namespace std;
class calculate{
public:
	double price;
	double num;
	virtual double result(void){
	}	
};
class normal:public calculate{
public:
	double result(void){
		return price*num;
	}	
};
class discount:public calculate{
public:
	double result(void){
	return price*num*0.8;
	}
};
int main ()
{
	calculate *a;
	int date;
	cout<<"please input the date:"<<endl;
	cin>>date; 
	if (date==11){
		a = new discount;
	} else {
		a = new normal;
	}	
	a->num=1;
	a->price=2;
	cout<<a->result()<<endl;
	return 0;
}

継承とポリモーフィズムを使用して、double 11 は計算クラスから継承された別のクラスに抽象化され、通常も計算クラスから継承された別のクラスに抽象化されます。結果の実装をサブクラスで提供します。

Double 12 がある場合、どのように書けばよいですか? 別の double 12 クラスを作成し、calculate クラスから継承して独自の結果計算を実装します。

「メイン関数 main で if else 判定を行う必要があるのでは?」と疑問に思う人もいるかもしれません。最初の関数との違いは何ですか?

違いは、新しい要件を追加するときに元のコードを変更する必要がなくなり、 (元のコードは計算のコア部分を指します)、元のコードの特性が完全に吸収されることです。

特定の関数が必要ない場合は、対応するクラスを削除するだけで済みます。これは柔軟でスケーラブルです。

コードが単純であるため、ここでは違いがないようです. 複雑な機能を実装する場合、コードのテスト後は触れるべきではありません. 最初の方法は、コア部分を継続的に変更するため、大きな隠れた危険性をもたらします。元のコードは複雑で修正が困難です。

ここで強調しておきたいのは、オブジェクト指向プログラミング言語でコードを記述するだけでは、プログラムが自動的にオブジェクト指向になるわけではなく、オブジェクト指向プログラミングのさまざまなメリットが得られない可能性があるということです。

したがって、オブジェクト指向はプログラミング言語ではなく思考に焦点を当てています。2 番目のセクションでは、Linux カーネルがオブジェクト指向の思考を C 言語でどのように具体化するかについて説明します。

1.3 オブジェクト指向とは、カプセル化、継承、およびポリモーフィズムを指しますか?

実際、1.1 の大企業の例と 1.2 の 2 つの異なる C++ プログラムの例から、カプセル化、継承、ポリモーフィズムはオブジェクト指向プログラミングの特徴にすぎず、中心的な概念ではないことがわかります

オブジェクト指向プログラミングの最も基本的なポイントは、シールドして隠すことです。

各部門は比較的独立しており、独自の憲章、仕事の方法、規則があります。独立とは「内部状態を隠す」ことを意味します。たとえば、憲章に従って何かをするために特定の部門に申請するように言うことはできますが、部門の誰にいつまでにそれを行うように指示することはできません。こうした内部の詳細は見ることができず、制御することもできません。

常に心に留めておくべきことの 1 つは、オブジェクト指向は、複雑性の高い大規模なプログラムを構築するのに便利であるということです。

2. Linux カーネルにおけるオブジェクト指向思考の具現化

2.1 パッケージ

カプセル化の定義は、プログラム内のオブジェクトのプロパティと実装の詳細を隠し、インターフェイスのみを外の世界に公開し、プログラム内のプロパティの読み取りと変更のアクセス レベルを制御することです。抽象化されたデータと動作を結合します (または、関数)を有機的に形成する 全体、つまりデータとそのデータを操作するためのソースコードを有機的に組み合わせて「クラス」を形成し、データと関数の両方がクラスのメンバーであること。

オブジェクト指向のカプセル化では、データとメソッド (関数) がまとめられます。

C 言語では、変数 int を定義し、その後、多くの関数 fun1、fun2、fun3 を定義します。

これらの関数はポインターを介して a を変更できますが、これらの関数でさえ a と同じ .c ファイル内にあるとは限らないため、特に混乱を招きます。

ただし、カプセル化を行うこともできます。

struct file {
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
    略去一部分
}

たとえば、Linux カーネルの struct ファイルには、ファイルのさまざまな属性が含まれているほか、ファイルの一連の操作関数である file_operrations 構造体も含まれています。

struct file_operations {
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	int (*open) (struct inode *, struct file *);
    略去一部分
} 

file_operations 構造体は関数ポインターの束であり、実際の操作関数ではありません。これはポリモーフィズムを実現するためです。

実際、この例も継承に非常に似ており、struct file は struct file_operations からすべてを継承しますが、継承をよりよく反映するために他の例を使用します。

2.2 継承

特殊クラス (またはサブクラス、派生クラス) のオブジェクトは、その一般クラス (または親クラス、基本クラス) のすべての属性とサービスを持ちます。これは、一般クラスからの特殊クラスの継承と呼ばれます。

継承の考え方と目的の観点から見ると、サブクラスが親クラスのデータとメソッドを共有できるようにすると同時に、親クラスに基づいて新しいデータ メンバーとメソッドを定義および拡張できるようにすることです。これにより、クラスの繰り返し定義が不要になり、ソフトウェアの再利用性が向上します。

C言語のリンクリスト構造は次のとおりです。

struct A_LIST {
    data_t        data; // 不同的链表这里的data_t类型不同。
    struct A_LIST    *next;
};

Linux カーネルには一般的なリンク リスト構造があります。

struct list_head {
    struct list_head *next, prev;
);

この構造は基本クラスとみなすことができ、その基本操作はリンク リスト ノードの挿入と削除、リンク リストの初期化と移動などです。他のデータ構造 (サブクラスとみなすことができる) を二重リンク リストに編成する場合、この一般的なリンク リスト オブジェクトをリンク リスト ノード (継承とみなすことができる) に含めることができます。

上の例のように、宣言するだけで済みます。

struct A_LIST {
    data_t            data;
    struct list_head    *list;
};

リンク リストの本質は線形シーケンスであり、その基本操作は挿入や削除などです。さまざまなリンク リストの違いは各ノードに格納されるデータ型にあるため、リンク リストの特性はこの一般的な形式に抽象化されます。親クラスとして存在するリンク リスト。リンク リストは、この親クラスの基本メソッドを継承し、独自の属性を拡張します。

コネクタとして、一般的なリンク リストはそれ自体の構造のみを担当し、実際に属する構造に注意を払う必要はありません。継承の場合と同様、親クラスのメソッドはサブクラスのメンバーを操作できませんし、操作する必要もありません。

リンクリスト構造のホストポインタの取得方法については、

構造体内の構造体タイプ TYPE のメンバー MEMBER のオフセットを取得します。

#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)

メンバー member を指すポインター ptr を通じてメンバー構造体型のポインターを取得します。

#define container_of(ptr, type, member) ({          \
    const typeof(((type *)0)->member)*__mptr = (ptr);    \
         (type *)((char *)__mptr - offsetof(type, member)); })

2.3 ポリモーフィズム 

Linux におけるポリモーフィズムの最も明白な例は、キャラクター デバイス アプリケーション プログラムとドライバー プログラムの間の対話です。アプリケーション プログラムは、open、read、write などの関数を呼び出してデバイスを開いて動作させますが、open、read、write の方法は気にしません。これらの機能の実装はドライバプログラム内にあり、デバイスごとにopen、read、writeの機能が異なり、ポリモーフィズムにおけるランタイムポリモーフィズムと同じ機能を実現しています。

プロセスの簡素化は実際には異なるドライバーの実装です

構造体 file_operations drv_opr1

構造体 file_operations drv_opr2

構造体 file_operations drv_opr3

アプリケーションの実行中に、デバイス番号に従って対応する struct file_operations を見つけて、それをポインタで指し、対応する struct file_operations 内の open、read、write 関数を呼び出します (実際の処理はこれより複雑です)。 。

オブジェクト指向プロトタイプを含む C プログラム:

#include<stdio.h>
double normal_result(double price,double num)
{
	return price * num; 
}
double discount_result(double price,double num)
{
	return price * num * 0.8; 
}

struct calculate{
	double price;
	double num;
	double (*result)(double price,double num);
};

int main ()
{
	struct calculate a;
	int date;
	a.price=1;
	a.num=2;
	printf("please input the date:\n");
	scanf("%d",&date);
	if(date==11)
		a.result=discount_result;
	else
		a.result=normal_result;
	printf("%lf\n",a.result(a.price,a.num));
	return 0;
}

おすすめ

転載: blog.csdn.net/freestep96/article/details/127356581