ホーム » 2024

年別アーカイブ: 2024

2024年7月
 123456
78910111213
14151617181920
21222324252627
28293031  

検索・リンク

差分とフィードバック制御

情報制御基礎の授業を通して、入力値を制御するため、コンピュータを使う場合の数値処理の基礎的な話として、信号の平滑化を説明してきたので、最後に差分について説明をする。また、実際には、入力値を制御に利用する一般的な構成のフィードバック制御について説明する。

変化の検出

例えば、以下のような若干のノイズが混ざった入力信号が与えられたとする。この波形で「大きな山が何ヶ所ありますか?」と聞かれたら、いくつと答えるべきであろうか?山の判断方法は色々あるが、4カ所という答えは、1つの見方であろう。では、この4カ所という判断はどうすればいいだろうか?

こういった山の数を数えるのであれば、一定値より高いか低いか…という判断方法もあるだろう。この絵であれば、15ステップ目、32ステップ目付近は、100を越えていることで、2つの山と判断できるだろう。

こういった予め決めておいた値より「上か?/下か?」で判断するときの基準値は、しきい値(閾値:threshold)と呼ぶ。

しかし、この閾値では、40ステップ目から50ステップ目も100を越えており、以下のようなプログラムを書いたら、40ステップ目~50ステップ目すべてをカウントしてしまう。

#define THRESHOLD 100
int x[ 100 ] = {
   // 波形のデータが入っているとする。
} ;

int count = 0 ;
for( int i = 0 ; i < 100 ; i++ ) {
   if ( x[i] >= THRESHOLD )
      count++ ;
}

また、65ステップ目の小さな山も1個とカウントしてしまう。

この問題を避けるために、閾値を130にすると、今度は最初の2つの山をカウントできない。どうすれば、山の数をうまくカウントできるのだろうか?

差分を求める

前述のような問題で山の数を数える方法を考えていたが、数学で山を見つける時には、何をするだろうか?

数学なら、山や谷の頂点を求めるのならば、微分して変化量が0となる場所を求めることで、極大値・極小値を求めるだろう。そこで、山を見つけるために入力値の変化量を求めてみよう。

表計算ソフトで差分を計算するのであれば、セルに図のような式を入力すればいいであろう。このようなデータ点で前の値との差差分と呼ぶ。数学であれば、微分に相当する。

このグラフを見ると、波形が大きく増加する部分で、差分が大きな正の値となる。さらに波形が大きく減少する部分で差分が負の大きな値となる。特にこのデータの場合、山と判断したい部分は差分が20以上の値の部分と定義することも考えられる。

#define TH_DIFF 20
int x[ 100 ] = {
   // 波形のデータが入っているとする。
} ;

int count = 0 ;
for( int i = 0 ; i < 100 ; i++ ) {
   if ( x[i] - x[i-1] >= TH_DIFF
        && x[i+1] - x[i] <= -TH_DIFF )
      count++ ;
}

しかし、このプログラムでは、山の数をうまくカウントしてくれない。うまく、山の数を数えるためには、差分の値を山と判断するための閾値(この場合は20)を調整することになるだろう。

移動平均との差

前回の講義で示したデータの例で、移動平均を取ると分かる事例ということで、船につけられた加速度センサーで、長い周期の波による船の揺れと、短い周期のエンジンによる振動があったとき、エンジンの振動を移動平均で取り除くことができるという事例を示した。

これを逆手にとれば、元の信号と移動平均の差を取れば、エンジンの振動だけを取り出すことも可能となる。以下は、前の事例で、前後5stepの移動平均(水色線)と元信号(青線)の差をとったものが緑線となっている。このような方法をとれば、元信号の短い周期の変動を抽出することができる。

制御工学の概要

以下に、制御工学ではどのようなことを行うのか、概要を述べる。
ここで紹介する制御理論は、古典制御理論と呼ばれる。

制御工学では、入力値と、何らかの処理を施し出力値が得られるシステムで、どのように制御するかを考える。

例えば、電気ポットの温度制御をする場合、設定温度の値を入力値とし、何らかの処理を行い、出力となるヒーターの電流を制御し、最終的には温度が測定される。ヒーターは、設定温度と温度計の値の差に応じて電流量を変化させる。このように一般的な制御では、最終的な温度が入力に戻っている。このように目標値に近づけるために、目標値との差に応じて制御することをフィードバック制御という。


制御の仕方には様々な方法があるが、 がとある時間で0からYに変化した場合を考える。入力と出力で制御された波形の例を示す。

この波形では、黒のように入力値が変化した場合、それに追いつこうと出力が変化する。(1)理想的には、速やかに追いつく赤のように変化したい。しかし、(2)慎重に制御をする人なら、変化への制動が大きい過制動(青点線)となり、目標値に追いつくまでに時間がかかる。(3)一方、すこしでもずれたら直そうとする人なら、時間的には速い反応ができるかもしれないが、目標値を追い越したり、増えすぎ分を減らしすぎたりして脈動する過制御(赤点線)となるかもしれない。

PID制御

目標値、出力、ずれ(偏差)、制御量とした時、基本的なフィードバック制御として偏差の使い方によってP動作,I動作,D動作がある。参考 Wikipedia PID制御

比例制御(P制御)

偏差に比例した制御を行う方式(を比例ゲインと呼ぶ)

今年のコロナ騒動を例にとるならば、比例制御は、今日の感染者数y(t)と目標としたい感染者数x(t)の差に応じて、対策の強さu(t)を決めるようなもの。

積分制御(I制御)

偏差のある状態が長い時間続く場合、入力値の変化を大きくすることで目標値に近づけるための制御。(は積分ゲイン)

積分制御は、目標の感染者数x(t)を感染者数y(t)が超えた累積患者数に応じて、対策を決めるようなもの。
移動平均は、一定範囲の値の和(を範囲のデータ数で割ったもの)であり、積分制御は移動平均の値に応じて制御するとみなすこともできる。

微分制御(D制御)

急激な出力値の変化が起こった場合、その変化の大きさに応じて妨げようとする制御。(は微分ゲイン)

微分制御は、目標数と感染者数の差が、前日よりどのぐらい増えたか(患者の増減の量:変化量)に応じて、対策を決めるようなもの。

PID制御

上記のI制御やD制御だけでは、安定させることが難しいので、これらを組み合わせたPID制御を行う。

この中で、の値は、制御が最も安定するように調整を行うものであり、数値シミュレーションや、ステップ応答を与えた時の時間的変化を測定して調整を行う。

オブジェクト指向とソフトウェア工学

オブジェクト指向プログラミングの最後の総括として、 ソフトウェア工学との説明を行う。

トップダウン設計とウォーターフォール型開発

ソフトウェア工学でプログラムの開発において、一般的なサイクルとしては、 専攻科などではどこでも出てくるPDCAサイクル(Plan, Do, Check, Action)が行われる。 この時、プログラム開発の流れとして、大企業でのプログラム開発では一般的に、 トップダウン設計とウォーターフォール型開発が行われる。

トップダウン設計では、全体の設計(Plan)を受け、プログラムのコーディング(Do)を行い、 動作検証(Check)をうけ、最終的に利用者に納品し使ってもらう(Action)…の流れで開発が行われる。設計(Plan)の中身は、要件定義機能仕様動作仕様…といった細かなフェーズになることも多い。 この場合、コーディングの際に設計の不備が見つかり設計のやり直しが発生すれば、 全行程の遅延となることから、前段階では完璧な設計が必要となる。 このような、上位設計から下流工程にむけ設計する方法は、トップダウン設計などと呼ばれる。また、処理は前段階へのフィードバック無しで次工程へ流れ、 川の流れが下流に向かう状態にたとえ、ウォーターフォールモデルと呼ばれる。

引用:Think IT 第2回開発プロセスモデル

このウォーターフォールモデルに沿った開発では、横軸時間、縦軸工程とした ガントチャートなどを描きながら進捗管理が行われる。

引用:Wikipedia ガントチャート

V字モデル

一方、チェック工程(テスト工程)では、 要件定義を満たしているかチェックしたり、基本設計や詳細設計が仕様を満たすかといったチェックが存在し、テストの前工程とそれぞれ対応した機能のチェックが存在する。 その各工程に対応したテストを経て最終製品となる様は、V字モデルと呼ばれる。

引用:@IT Eclipseテストツール活用の基礎知識

しかし、ウォーターフォールモデルでは、(前段階の製作物の不備は修正されるが)前段階の設計の不備があっても前工程に戻るという考えをとらないため、全体のPDCAサイクルが終わって次のPDCAサイクルまで問題が残ってしまう。巨大プロジェクトで大量の人が動いているだから、簡単に方針が揺らいでもトラブルの元にしかならないことから、こういった手法は大人数での巨大プロジェクトでのやり方である。

ボトムアップ設計とアジャイル開発

少人数でプログラムを作っている時(あるいはプロトタイプ的な開発)には、 部品となる部分を完成させ、それを組合せて全体像を組み上げる手法もとられる。 この方法は、ボトムアップ設計と呼ばれる。このような設計は場当たり的な開発となる場合があり設計の見直しも発生しやすい。

また、ウォーターフォールモデルでは、前工程の不備をタイムリーに見直すことができないが、 少人数開発では適宜前工程の見直しが可能となる。 特にオブジェクト指向プログラミングを実践して隠蔽化が正しく行われていれば、 オブジェクト指向によるライブラリの利用者への影響を最小にしながら、ライブラリの内部設計の見直しも可能となる。 このような外部からの見た挙動を変えることなく内部構造の改善を行うことリファクタリングと呼ばれる。

一方、プログラム開発で、ある程度の規模のプログラムを作る際、最終目標の全機能を実装したものを 目標に作っていると、全体像が見えずプログラマーの達成感も得られないことから、 機能の一部分だけ完成させ、次々と機能を実装し完成に近づける方式もとられる。 この方式では、機能の一部分の実装までが1つのPDCAサイクルとみなされ、 このPDCAサイクルを何度も回して機能を増やしながら完成形に近づける方式とも言える。 このような開発方式は、アジャイルソフトウェア開発と呼ぶ。 一つのPDCAサイクルは、アジャイル開発では反復(イテレーション)と呼ばれ、 短い開発単位を反復し製品を作っていく。この方法では、一度の反復後の実装を随時顧客に見てもらうことが可能であり、顧客とプログラマーが一体となって開発が進んでいく。

引用:コベルコシステム

エクストリームプログラミング

アジャイル開発を行うためのプログラミングスタイルとして、 エクストリームプログラミング(Xp)という考え方も提唱されている。 Xpでは、5つの価値(コミュニケーション,シンプル,フィードバック,勇気,尊重)を基本とし、 開発のためのプラクティス(習慣,実践)として、 テスト駆動開発(コーディングでは最初に機能をテストするためのプログラムを書き、そのテストが通るようにプログラムを書くことで,こまめにテストしながら開発を行う)や、 ペアプログラミング(2人ペアで開発し、コーディングを行う人とそのチェックを行う人で役割分担をし、 一定期間毎にその役割を交代する)などの方式が取られることが多い。

リーン・ソフトウェア開発は、トヨタ生産方式を一般化したリーン生産方式をソフトウェア開発に導入したもの。ソフトウェアでよく言われる話として「完成した機能の64%は使われていない」という分析がある。これでは、開発に要する人件費の無駄遣いとみることもできる。そこで、品質の良いものを作る中で無駄の排除を目的とし、本当にその機能は必要かを疑いながら、優先順位をつけ実装し、その実装が使われているのか・有効に機能しているのかを評価ながら開発をすすことが重要であり、リーン生産方式がソフトウェア開発にも取り込まれていった。

アジャイルの問題点

伽藍(がらん)とバザール

これは、通常のソフトウェア開発の理論とは異なるが、重要な開発手法の概念なので「伽藍とバザール」を紹介する。

伽藍(がらん)とは、優美で壮大な寺院のことであり、その設計・開発は、優れた設計・優れた技術者により作られた完璧な実装を意味している。バザールは有象無象の人の集まりの中で作られていくものを意味している。

たとえば、伽藍方式の代表格である Microsoft の製品は、優秀なプロダクトだが、中身の設計情報などを普通の人は見ることはできない。このため潜在的なバグが見つかりにくいと言われている。

これに対しバザール方式の代表格の Linux は、インターネット上にソースコードが公開され、誰もがソースコードに触れプログラムを改良してもいい(オープンソース)。その中で、新しい便利な機能を追加しインターネットに公開されれば、良いコードは生き残り、悪いコードは自然淘汰されていく。

このオープンソースを支えているツールとしては、プログラムの変更履歴やバージョン管理を行う分散型バージョン管理システム git が有名であり、Linux のソフトウェア管理などで広く利用されている。。

オープンソースライセンス

バザール方式は、オープンソースライセンスにより成り立っていて、このライセンスが適用されていれば、改良した機能はインターネットに公開する義務を引き継ぐ。このライセンスの代表格が、GNU パブリックライセンス(GPL)であり、公開の義務の範囲により、BSD ライセンスApacheライセンスといった違いがある。

コピーレフト型 GNU ライセンス(GPL) 改変したソースコードは公開義務,
組み合わせて利用で対応箇所の開示。
準コピーレフト型 LGPL, Mozilla Public License 改変したソースコードは公開義務。
非コピーレフト型 BSDライセンス, Apacheライセンス ソースコードを改変しても必ずしもすべてを公開しなくてもいい。

GPLライセンスのソフトウェアを組み込んで製品を開発した場合に、ソースコード開示を行わないとGPL違反となる。大企業でこういったGPL違反が発生すると、大きな風評被害による損害をもたらす場合がある。

また、最近では、機械学習などのAI技術によりプログラムを自動生成してくれる技術が出てきている。この際のプログラムの学習には、GitHub のようなソフトウェア開発環境のオープンソースのプログラムが使われている。このため、CopilotChatGPT などを使いながらプログラムを作成していると、知らないうちにGPLライセンスのソースコードが混入する可能性も出てきた。この場合、自社開発のソフトが知らないうちにGPLライセンス違反に抵触し、後で訴えられる可能性が出てきている。

ライブラリと分割コンパイル

巨大なプログラムを作ってくると、プログラムのコンパイルに時間がかかる。こういった場合には、共有できる処理であればライブラリにまとめたり、分割コンパイルといった方法をとる。

C言語とライブラリ

C言語でプログラムを作っている時、printf() や scanf() といった関数を使うが、こういった組み込み関数のプログラムはどこに書かれているのだろうか?

ソースプログラムがコンパイルする際には、コンパイラ(compiler)によるコンパイル処理(compiler)リンカ(linker or linkage editor)によるリンク処理(link)が行われる。この時に、printf()やscanf() の機械語(組み込み関数などのライブラリの内容)が実行プログラム a.out の中に埋め込まれる。通常は、コンパイルとリンク処理は一連の処理として実行される。

helloworld.c ソースプログラム
  ↓ compiler $ gcc -c helloworld.c  (コンパイルだけ行う)
 ↓       ただしC言語では、コンパイルの前にプリプロセッサ処理 #include,#define の処理が行われる。
helloworld.o オブジェクトファイル(中間コード)
  ↓ linker   $ gcc helloworld.o     (リンク処理を行う)
 (+) ← libgcc.a ライブラリ(printf, scanf....)
  ↓          $ ./a.out
a.out 実行プログラム

静的リンクライブラリと動的リンクライブラリ

しかし、printf() や scanf() のような組み込み関数の機械語が実行プログラムの中に単純に埋め込まれると、

  • よく使われるprintf()やscanf()の処理は、沢山の実行プログラムの中に埋め込まれる。
    そして、組み込み関数を使ったプログラムが複数実行されると、実行中のメモリに複数の組み込み関数の処理が配置されてメモリの無駄が発生する。
  • その組み込み関数に間違いがあった場合、その組み込み関数を使った実行プログラムをすべて再コンパイルしないといけなくなる。

リンクされたプログラムの機械語が実行プログラムに埋め込まれる方式は、静的リンクライブラリと呼ぶ。

しかし、静的リンクライブラリの方式は、実行時の命令の領域のムダや、ライブラリに間違いがあった場合の再コンパイルの手間があるため、動的リンクライブラリ方式(共有ライブラリ方式)がとられる。

動的リンクライブラリでは、プログラム内には動的リンクを参照するための必要最小限の命令が埋め込まれ、命令の実体は OS が動的リンクライブラリとして管理する。

Linux では、静的リンクライブラリのファイルは、lib~.a といった名前で保存され、動的リンクライブラリは、lib~.so という名前で保存されている。 Windows であれば、拡張子が ~.DLL のファイルが動的リンクライブラリである。

OS にとって必須の動的リンクライブラリは /lib 配下に保存されるが、ユーザが独自にインストールするパッケージの場合 /lib のアクセス権限の都合で別の場所に保存されるかもしれない。この場合、その独自パッケージを起動する時に、動的リンクライブラリの保存場所を見つけ出す必要がある。Linux では 環境変数 LD_LIBRARY_PATH に自分が利用する動的リンクライブラリの保存場所を記載すれば、OS がプログラム起動時に動的リンクライブラリを見つけてくれる。

Javaのコンパイル と JIT と CLASSPATH

Java のプログラムを実行する場合は、以下のような流れで実行される。

Main.java ソースプログラム
 ↓ コンパイラ $ javac Main.java
Main.class Javaバイトコード
 ↓ JVM実行    $ java Main (Main.classを実行)
JIT (Just In Time コンパイラ)
 ↓ バイトコードを機械語に変換
機械語を実行

この場合、複数のファイルに分割された Java のプログラムであれば、以下のように分割、実行となる。

((( ListNode.java )))
import java.util.* ;

class ListNode {
   int      data ;
   ListNode next ;
   ListNode( int d , ListNode n ) {
       this.data = d ;
       this.next = n ;
   }
    static void print( ListNode p ) {
        for( ; p != null ; p = p.next )
            System.out.print( p.data + " " ) ;
        System.out.println() ;
    }
} ;

((( Main.java )))
import java.util.* ;
public class Main {
    public static void main(String[] args) throws Exception {
        ListNode a = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ;
        ListNode.print( a ) ;
    }
}

((( コンパイルと実行 )))
$ javac ListNode.java
$ javac Main.java
$ java  Main

この時、ListNode.java , ListNode.class を別フォルダ(例えば module/ListNode.java に保存する場合、java Main にてプログラムを実行する際に、ListNode.class が見つからないといったエラーが出てくる。こういった場合には、javac –classpath module Main.java とか、java –classpath module Main といったように実行するか、環境変数 CLASSPATH に、export CLASSPATH=module:その他… といったように実行に必要な class ファイルが保存されている場所を登録しておく必要がある。

分割コンパイル

複数人でプログラムを開発する場合、1つのファイルを全員で編集するのは混乱してしまう。例えば、ちょうど情報構造論で説明している、リスト処理のようなプログラムであれば、List 構造の構造体、cons(),print() といったList 構造を操作する関数を作る人と、そのそれらの関数を利用するプログラムを書く人に分かれてプログラム開発をすれば混乱も減らせる。そして、それぞれ別のファイルになっている方が開発しやすい。

  • list.h : ヘッダファイル – 構造体の宣言や関数のプロトタイプ宣言や変数のextern宣言などを記載
  • list.c : リスト処理の cons,print などの処理内容を記載
  • main.c : cons,print を使った処理を記載

#include “ヘッダファイル”

自作のヘッダファイルを読み込む場合は、#include list.h のように記載する。

#include で、ヘッダファイルを < > で囲むと、/usr/include フォルダから探してくれる” “ で囲むと、ソースプログラムと同じフォルダの中からヘッダファイルを探す

プロトタイプ宣言と extern 宣言

ヘッダファイルには、list.c と main.c の両方で使われるデータ構造、関数、変数の宣言を記載する。関数は、引数の型や返り値の型を記載した struct List* cons( int , struct List*) ; といったプロトタイプ宣言を記載する。変数については、変数の型だけを宣言する extern struct List* stack ; といった extern 宣言を記載する。

// list.h -----------------------------
// リスト構造の宣言
struct List {
  int data ;
  struct List* next ;
} ;

// リスト操作の関数のプロトタイプ宣言
extern struct List* cons( int , struct List* ) ;
extern void print( struct List* ) ;

// stack の extern 宣言
extern struct List* stack ;

// スタック操作関数のプロトタイプ宣言
extern void push( int ) ;
extern int  pop() 
// list.c -----------------------------
#include <stdio.h>
#include <stdlib.h>

#include "list.hxx"

// リストの要素を作る
struct List* cons( int x , struct List* n ) {
  struct List* ans = (struct List*)malloc( sizeof( struct List ) ) ;
  if ( ans != NULL ) {
    ans->data = x ;
    ans->next = n ;
  }
  return ans ;
}
// 全要素の出力
void print( struct List* p ) {
  for( ; p != NULL ; p = p->next )
    printf( "%d " , p->data ) ;
  printf( "\n" ) ;
}
// stack の実体
struct List* stack = NULL ;
// スタックに x を保存
void push( int x ) {
  stack = cons( x , stack ) ;
}
// スタックの先頭を取り出す
int pop() {
  int ans = stack->data ;
  struct List* d = stack ;
  stack = stack->next ;
  free( d ) ;
  return ans ;
}
// main.c -----------------------------
#include <stdio.h>

#include "list.hxx"

int main() {
  struct List* top = cons( 1 , cons( 2 , cons( 3 , NULL ) ) ) ;
  print( top ) ;

  push( 11 ) ; push( 22 ) ; push( 33 ) ;
  printf( "%d\n" , pop() ) ;
  printf( "%d\n" , pop() ) ;
  printf( "%d\n" , pop() ) ;
  return 0 ;
}

分割コンパイルの作業を確認するために、以下のコマンドを実行してみよう。

((( 一度にコンパイルする方法 )))
guest00@nitfcei:~$ cp /home0/Challenge/seg-compile/* .
guest00@nitfcei:~$ gcc list.c main.c
guest00@nitfcei:~$ ./a.out
# 正しく実行できる。
 
((( 失敗するコンパイル )))
guest00@nitfcei:~$ gcc list.c
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status
# list.c の中に main() が無いからエラー
 
guest00@nitfcei:~$ gcc main.c
/usr/bin/ld: /tmp/ccxr4Fif.o: in function `main':
main.c:(.text+0x17): undefined reference to `cons'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `cons'
/usr/bin/ld: main.c:(.text+0x41): undefined reference to `print'
:
collect2: error: ld returned 1 exit status
# main.c の中に、cons(),print(),push(),pop() が無いからエラー
 
((( プログラムをひとつづつコンパイル )))
guest00@nitfcei:~$ gcc -c list.c           # list.o を作る
guest00@nitfcei:~$ gcc -c main.c           # main.o を作る
guest00@nitfcei:~$ gcc list.o main.o       # list.o と main.o から a.out を作る
guest00@nitfcei:~$ ./a.out
# 正しく実行できる。

make と Makefile

上記のように分割コンパイルのためにファイルを分割すると、実行プログラムを生成するには以下のコマンドを実行する必要がある。

  1. gcc -c list.c        (list.o を作る)
  2. gcc -c main.c     (main.o を作る)
  3. gcc list.o main.o (list.oとmain.oを使って a.out を作る)

また、プログラムのデバッグ作業中ですでに list.o , main.o , a.out が生成されている状態で、main.c の中に間違いを見つけて修正した場合、list.o を作るための 手順1 は不要となる。例えば list.c が巨大なプログラムであれば、手順1を省略できれば、コンパイル時間も短くできる。一方で、どのファイルを編集したから、どの手順は不要…といった判断をプログラマーが考えるのは面倒だったりする。

こういった一部の修正の場合に、必要最小限の手順で目的の実行プログラムを生成するためのツールが make であり、どのファイルを利用してどのファイルが作られるのかといった依存関係と、どういった手順を実行するのかといったことを記述するファイルが Makefile である。

### Makefile ###
# a.out を作るには list.o , main.o が必要
a.out:  list.o main.o      # 最終的に生成する a.out の依存関係を最初に書く
        gcc list.o main.o

# list.o は list.c , list.h に依存
list.o: list.c list.h
        gcc -c list.c

# main.o は main.c , list.h に依存
main.o: main.c list.h
        gcc -c main.c

clean:; rm *.o a.out       # 仮想ターゲット: make clean と打つと、rm *.o a.out を実行してくれる。

Makefile では、依存関係と処理を以下の様に記載する。make コマンドは、ディレクトリ内の Makefile を読み込み、ターゲットファイルのタイムスタンプと依存ファイルのタイムスタンプを比較し、依存ファイルの方が新しい場合(もしくはターゲットファイルが無い場合)、ターゲットを生成するための処理が行われる。

ターゲットファイル: 依存ファイル ...
      ターゲットファイルを生成する処理    # 行の先頭の空白は"タブ"を使うこと

理解確認

集合とリスト処理

リスト構造は、必要に応じてメモリを確保するデータ構造であり、データ件数に依存しないプログラム が記述できる。その応用として、集合処理を考えてみる。集合処理の記述には、2進数を使った方式リストを用いた方法が一般的である。以下にその処理について示す。

bit演算子

2進数を用いた集合処理を説明する前に、2進数を使った計算に必要なbit演算子について復習してみる。

bit演算子は、その数値を2進数表記とした時の各ビットをそれぞれAND,OR,EXOR,NOTなどの計算を行う。

bit演算子 計算の意味 関連知識
& bit AND 3 & 5
0011)2 & 0101)2= 0001)2
論理演算子
if ( a == 1 && b == 2 ) …
| bit OR 3 | 5
0011)2 | 0101)2= 0111)2
論理演算子
if ( a == 1 || b == 2 ) …
~ bit NOT ~5
~ 00..00,0101)2= 11..11,1010)2
論理否定演算子
if ( !a == 1 ) …
^ bit EXOR 3 ^ 5
0011)2 ^ 0101)2= 0110)2
<< bit 左シフト 3 << 2
0011)2 << 2 = 001100)2
x << y は と同じ
>> bit 右シフト 12 >> 2
1100)2 >> 2 = 11)2
x >> y は  と同じ
import java.util.*;

public class Main {
   public static void main(String[] args) throws Exception {
      System.out.println( 12 & 5 ) ;    // 1100 & 0101 = 0100 = 4
      System.out.println( 12 | 5 ) ;    // 1100 | 0101 = 1101 = 13
      System.out.println( ~12 & 0xF ) ; // ~1100 & 1111 = 0011 = 3
      System.out.println( 3 << 2 ) ;    // 0011 << 2 = 1100
      System.out.println( 12 >> 2 ) ;   // 1100 >> 2 = 0011
      System.out.println( ~12 + 1 ) ;   // ~0..00001100 + 1 = 1..11110011 + 1 = 1..11110100 = -12
   }
}

2進数とビットフィールド

例えば、誕生日のの情報を扱う際、20230726で、2023726を表現することも多い。

しかしこの方法は、この年月日の情報から年(4桁)、月(2桁)、日(2桁)を取り出す処理では、乗算除算が必要となる。通常のCPUであれば、簡単な乗除算は速度的にも問題はないが、組込み系では処理速度の低下も懸念される。

int ymd = 20230726 ;
int y , m , d ;
y = ymd / 10000 ;
m = ymd / 100 % 100 ;
d = ymd % 100 ;

y = 1965 ; m = 2 ; d = 7 ;
ymd = y * 10000 + m * 100 + d ;

こういった処理を扱う際には、2進数の考え方を使って扱う方法がある。
例えば、年は 0..2047 の範囲と考えれば 11 bit で表現でき、月は1..12の範囲であり 4bit で表現可能であり、日は1..31 で 5bit で表現できる。これを踏まえて、年月日を 11+4+5 = 20bit で表す(YYYY,YYYY,YYYM,MMMD,DDDD)なら、以下のプログラムのように書ける。

int ymd = (2024 << 9) + (7 << 5) + 26 ; // YYYY,YYYY,YYYM,MMMD,DDDD
int y , m , d ;                         // 1111,1101,0000,1111,1010
y = ymd >> 9 ;          // YYYYYYYYYYY
m = (ymd >> 5) & 0xF ;  // YYYYYYYYYYYMMMM & 000000000001111
d = (ymd & 0x1F) ;      // YYYYYYYYYYYMMMMDDDDD & 00000000000000011111

y = 1965 ; m = 2 ; d = 7 ;
ymd = (y << 9) + (m << 5) + d ;

C言語でのビットフィールド

しかし、上記のプログラムでは、いちいち2進数bit演算をイメージする必要があって、プログラムが分かりづらい。C言語では、こういった際にに使うのが ビットフィールドである。

// C言語の場合 (Javaではビットフィールドの構文がない)
struct YMD {
   unsigned int year  : 11 ; // ビットフィールドでは、
   unsigned int month :  4 ; // 構造体の要素を何ビットで保存するのか
   unsigned int day   :  5 ; // 指定することができる。
} ;
struct YMD ymd = { 2023 , 7 , 26 } ;
int y , m , d ;
y = ymd.year ;
m = ymd.month ;
d = ymd.day ;

ymd.year = 1965 ; ymd.month = 2 ; ymd.day = 7 ;

2進数を用いた集合計算

リストによる集合の前に、もっと簡単な集合処理を考える。

最も簡単な方法は、要素に含まれる=true か 含まれない=false を boolean型の配列に覚える方法であろう。数字Nが集合に含まれる場合は、配列[N]に true を覚えるものとする。この方法で積集合などを記述した例を以下に示す。

import java.util.*;

public class Main {
    public static void boolarray_print( boolean[] a ) {
        for( int i = 0 ; i < a.length ; i++ )
            System.out.print( a[i] ? "T" : "F" ) ;
        System.out.println() ;
    }
    public static void boolarray_and( boolean[] ans , boolean[] a , boolean[] b ) {
        for( int i = 0 ; i < a.length ; i++ )
            ans[i] = a[i] && b[i] ;
    }
    public static void boolarray_or( boolean[] ans , boolean[] a , boolean[] b ) {
        for( int i = 0 ; i < a.length ; i++ )
            ans[i] = a[i] || b[i] ;
    }
    public static void main(String[] args) throws Exception {
        //               0      1      2      3      4      5      6      7      8      9
        boolean[] ba = { false, true,  true,  true,  false, false, false, false, false, false } ; // {1,2,3}
        boolean[] bb = { false, false, true,  false, true,  false, true,  false, false, false } ; // {2,4,6}
        boolean[] bc = { false, false, false, false, true,  false, true,  false, false, true  } ; // {4,6,9}
        boolean[] ans = new boolean[ 10 ] ;
        boolarray_print( ba ) ;
        boolarray_print( bb ) ;
        boolarray_and( ans , ba , bb ) ;
        boolarray_print( ans ) ;
        
        boolarray_print( bb ) ;
        boolarray_print( bc ) ;
        boolarray_or( ans , bb , bc ) ;
        boolarray_print( ans ) ;
    }
}
FTTTFFFFFF // ba
FFTFTFTFFF // bb
FFTFFFFFFF // ba & bb
FFTFTFTFFF // bb
FFFFTFTFFT // bc
FFTFTFTFFT // bb | bc

しかし、上述のプログラムでは、要素に含まれる/含まれないという1bitの情報をboolean型で保存しているが、実体は整数型で保存しているためメモリの無駄となる。

データ件数の上限が少ない場合には、「2進数の列」の各ビットを集合の各要素に対応づけし、要素の有無を0/1で表現する。この方法を用いるとビット演算命令で 和集合、積集合を計算できるので、処理が極めて簡単になる。

2進数を用いた集合計算

扱うデータ件数が少ない場合には、「2進数の列」の各ビットを集合の各要素に対応づけし、要素の有無を0/1で表現する。この方法を用いるとC言語のビット演算命令で 和集合、積集合を計算できるので、処理が極めて簡単になる。

以下のプログラムは、0〜31の数字を2進数の各ビットに対応付けし、 ba = {1,2,3} , bb = {2,4,6} , bc= {4,6,9} を要素として持つ集合で、ba bb , bb  bc の計算を行う例である。

import java.util.*;

public class Main {
    static void bitfield_print( int x ) {
        for( int i = 0 ; i < 10 ; i++ )
            System.out.print( ((x & (1 << i)) != 0) ? "T" : "F" ) ;
        System.out.println() ;
    }
    public static void main(String[] args) throws Exception {
        int ba = (1 << 1) | (1 << 2) | (1 << 3) ; // {1,2,3}
        int bb = (1 << 2) | (1 << 4) | (1 << 6) ; // {2,4,6}
        int bc = (1 << 4) | (1 << 6) | (1 << 9) ; // {4,6,9}
        bitfield_print( ba ) ;
        bitfield_print( bb ) ;
        bitfield_print( ba & bb ) ;
        
        bitfield_print( bb ) ;
        bitfield_print( bc ) ;
        bitfield_print( bb | bc ) ;
    }
}

有名なものとして、エラトステネスのふるいによる素数計算を2進数を用いて記述してみる。このアルゴリズムでは、各bitを整数に対応付けし、素数で無いと判断した2進数の各桁に1の目印をつけていく方式である。

import java.util.*;

public class Main {
    static final int INT_BITS = 31 ;
    static int   prime = 0 ;
    public static void main(String[] args) throws Exception {
        // 倍数に非素数の目印をつける
        for( int i = 2 ; i <= INT_BITS ; i++ ) {
            if ( (prime & (1 << i)) == 0 ) {
                for( int j = 2 * i ; j <= INT_BITS ; j += i )
                    prime |= (1 << j) ;
            }
        }
        // 非素数の目印の無い値を出力
        for( int i = 2 ; i <= INT_BITS ; i++ ) {
            // 目印のついていない値は素数
            if ( (prime & (1 << i)) == 0 )
                System.out.println( i ) ;
        }
    }
}

リスト処理による積集合

前述の方法は、リストに含まれる/含まれないを、2進数の0/1で表現する方式である。しかし、2進数であれば、int で 31要素、long int で 63 要素が上限となってしまう。

しかし、リスト構造であれば、リストの要素として扱うことで、要素件数は自由に扱える。また、今までの授業で説明してきた cons() などを使って表現すれば、簡単なプログラムでリストの処理が記述できる。

例えば、積集合(a ∩ b)を求めるのであれば、リストa の各要素が、リストb の中に含まれるか find 関数でチェックし、 両方に含まれたものだけを、ans に加えていく…という考えでプログラムを作ると以下のようになる。

import java.util.*;

class ListNode {
   int      data ;
   ListNode next ;
   ListNode( int d , ListNode n ) {
       this.data = d ;
       this.next = n ;
   }
   static void print( ListNode p ) {
      for( ; p != null ; p = p.next )
         System.out.print( p.data + " " ) ;
      System.out.println() ;
   }
   static boolean find( ListNode p , int key ) {
      for( ; p != null ; p = p.next )
         if ( p.data == key )
            return true ;
      return false ;
   }
   static ListNode set_prod( ListNode a , ListNode b ) {
      ListNode ans = null ;
      for( ; a != null ; a = a.next ) {
         if ( find( b , a.data ) )
            ans = new ListNode( a.data , ans ) ;
      }
      return ans ;
   }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode b = new ListNode( 2 , new ListNode( 4 , new ListNode( 6 , null ) ) ) ;
        ListNode c = new ListNode( 4 , new ListNode( 6 , new ListNode( 9 , null ) ) ) ;
        ListNode b_and_c = ListNode.set_prod( b , c ) ;
        ListNode.print( b_and_c ) ;
    }
}

例題として、和集合差集合などを考えてみよう。

理解確認

  • 2進数を用いた集合処理は、どのように行うか?
  • リスト構造を用いた集合処理は、どのように行うか?
  • 積集合(A ∩ B)、和集合(A ∪ B)、差集合(A – B) の処理を記述せよ。

前期期末前の課題レポート

プログラムは書いて・動かして・間違って・直す が重要ということで、以下に前期期末試験前までに取り組むレポート課題をしめす。

レポート課題(プログラム例)

Java を用いて、後に示すデータ処理をするためのリスト構造を定義し、与えられたデータを追加していく処理を作成せよ。

課題の説明用に、複素数のリスト構造を定義し、指定した絶対値以下の複素数を抜き出す関数をつくった例を示す。

import java.util.*;

class ComplexListNode {
   double          re ;
   double          im ;
   ComplexListNode next ;
   ComplexListNode( double r , double i , ComplexListNode n ) {
       this.re = r ;
       this.im = i ;
       this.next = n ;
   }
} ;

public class Main {
    static ComplexListNode top = null ;
    static void print( ComplexListNode p ) {
        for( ; p != null ; p = p.next ) {
            System.out.println( "(" + p.re + ")+j(" + p.im + ")" ) ;
        }
    }
    static void add( double r , double i ) {
        top = new ComplexListNode( r , i , top ) ;
    }
    static ComplexListNode filter_lessthan( ComplexListNode p , double v_abs ) {
        ComplexListNode ans = null ;
        for( ; p != null ; p = p.next ) {
            if ( Math.sqrt( p.re * p.re + p.im * p.im ) <= v_abs )
                ans = new ComplexListNode( p.re , p.im , ans ) ;
        }
        return ans ;
    }
    public static void main(String[] args) throws Exception {
        add( 1.0 , 2.0 ) ;
        add( -1.0 , -1.0 ) ;
        add( 2.0 , -1.0 ) ;
        add( 1.0 , 0 ) ;
        print( top ) ;
        
        ComplexListNode less_than_2 = filter_lessthan( top , 2 ) ;
        System.out.println( "less than 2" ) ;
        print( less_than_2 ) ;
    }
}

((( 実行結果の例 )))
(1.0)+j(0.0)
(2.0)+j(-1.0)
(-1.0)+j(-1.0)
(1.0)+j(2.0)
less than 2
(-1.0)+j(-1.0)
(1.0)+j(0.0)

レポート内容

上記のプログラムをまねて、以下のレポート課題を作成すること。テーマは ((出席番号-1)%3+1) を選択すること。

  1. 年号のデータが、年号の名称と年号の始まりの年月日がYYYYMMDD形式で、”Meiji”,18681023 / ”Taisho”,19120730 / “Showa”,19261225 / “Heisei”,19890108 / “Reiwa”,20190501 の様に与えられる。このデータ構造を覚えるリスト構造を作成せよ。また ListNode のデータで、西暦の日付のリストが seireki_list = new ListNode( 19650207, new ListNode( 20030903 , null ) ) ; のように与えられたら、そのデータを和暦で表示するプログラムを作成せよ。 (参考2023年前期期末)
  2. 市町村名,月,日,最高気温,最低気温のデータが、”fukui”,8月,4日,27.6℃,22.3℃ / “fukui”,8月,5日,31.5℃,23.3℃ / “fukui”,8月,7日,34.7℃,25.9℃ / “obama”,8月,6日,34.2℃,23.9℃ の様に与えられる。このデータ構造で覚えるリスト構造を作成せよ。また、この中から真夏日(最高気温が30℃以上)でかつ熱帯夜(最低気温が25℃)の日のリストを抽出し表示するプログラムを作成せよ。(参考2022年前期期末)
  3. ホスト名と、IPアドレス(0~255までの8bitの値✕4個で与えるものとする)のデータ構造で、”www.fukui-nct.ac.jp”,104,215,54,205 / “perrine.tsaitoh.net”,192,168,11,2 / “dns.fukui-nct.ac.jp”,10,10,21,51 / “dns.google.com”,8,8,8,8 の様に与えられる。このデータ構造をリスト構造で覚えるプログラムを作成せよ。また、この中からプライベートアドレスのリストを抽出し表示するプログラムを作成せよ。プライベートアドレスは 10.x.x.x, 172.16~31.x.x,192.168.x.x とする。(参考2019年前期期末)

プログラムを作るにあたり、リスト構造には add( 与えられたデータ… ) のように呼び出してリストに追加すること。この時、生成されるリストが、登録の逆順になるか、登録順になるかは、自分の理解度に応じて選択すること。抽出する処理を書く場合も登録順序どおりにするかは自分の理解度に応じて選べばよい。

また、理解度に自信がある人は、add() などの処理を「オブジェクト指向」のように記述する方法を検討すること。
あくまで、リスト構造の理解を目的とするため、ArrayList<型> , List<型> のようなクラスは使わないこと。(ただし考察にて記述性の対比の対象として使うのはOK)

移動平均の処理

前回の授業で説明したようなA/D変換した数値データを読み取った場合、どのようなことが発生するか考える。

例えば、以下に示すような測定値があったとする。

このデータの一部をグラフ化してみると、次のような波形であった。

この波形をみると、大きく見ればsinカーブだが、細かい点を見るとデータにブレがある。

誤差の原因

このような測定結果が得られた場合、本来コンピュータで処理したいデータは何であろうか?

原因は様々なものが考えられるが、

  1. 回路のノイズ対策が不十分で、外部の電気的な影響が混入。
    オシロスコープで周期を図ると、60Hz なら、交流電源だったり…
  2. D/A 変換を行う場合には、量子化誤差かもしれない。

例えば、最初の波形が、加速度センサーの値であったとして、船の上で揺れているために、大きな周期で加速度が変化しているかもしれない。一方で、船自体がエンジンによる揺れで加速度が変化しているかもしれない。

船の中で波の揺れと、エンジンの揺れが観測されている加速度センサーの情報で、船の揺れの大きさ・揺れの周期を知りたい場合、どうすればいいだろうか?

移動平均を計算してみる

このデータを見ると、10個のデータまでの間で、波形が上下に変動している。船の揺れとエンジンの揺れが原因であれば、10個ぐらいのデータのゆらぎが、エンジンによる揺れと考えられる。では、この10個ぐらいの範囲で値が上下の影響を減らしたければ、どうすればいいか?一番簡単な方法は、前後10個のデータで平均を取ればいいだろう。増減する値を加えれば、プラスの部分とマイナスの部分の値が相殺されて0に近くはず。そこでは、Excel で前後データの平均をとってみよう。

Excelで前後11点の平均を求める式をセルに入れる

青線:元波形データ(B列)、赤線:前後11点の平均(C列)

このように、データの前後の決められた範囲の平均を平均する処理は、移動平均(単純移動平均)と呼ぶ。

時間tにおけるデータをとした場合、前後5点の移動平均は、以下のような式で表せるだろう。

単純移動平均

単純移動平均は、時刻tの平均を、その前後のデータで平均を求めた。この方式は、実際には与えられた波形のデータを全部記録した後に、単純移動平均をとる場合に有効である。

しかし、時々刻々変化する測定値の平均をその都度使うことを考えると、上記の方法は、未来の測定値を使っていることから、現実的ではない。

// 単純移動平均(未来の値も使う)
#define NS 3
int x[ SIZE ] ; // 入力値
int y[ SIZE ] ; // 出力値
for( int t = NS ; t < SIZE-NS ; t++ ) {
   int s = 0 ;
   for( int i = -NS ; i <= +NS ; i++ ) // 2*NS+1回の繰り返し
      s += x[t+i] ;
   y[t] = s / (2*NS + 1) ;
}

過去の値だけを使った移動平均

そこで、過去の値だけで移動平均をとることも考えられる。

この、単純移動平均と、過去の値だけを使う単純移動平均を、適当な測定値に対して適用した場合のグラフの変化を Excel によってシミュレーションした結果を以下に示す。

しかし、このグラフを見ると、波形後半の部分に注目するとよく分かるが、過去の値だけを使った移動平均では、測定値が立ち上がったのを追いかけて値が増えていく。これでは移動平均は時間的な遅れとなってしまう。

// 未来の値を使わない単純移動平均
for( int t = NS ; t < SIZE ; t++ ) {
   int s = 0 ;
   for( int i = 0 ; i <= NS ; i++ ) // NS+1回の繰り返し
      s += x[t-i] ;
   y[t] = s / (NS+1) ;
}こ

コロナ感染者数のデータの見せ方

昨年までは、コロナ感染者数の増減のグラフを見る機会が多かった。例えば、以下のようなグラフ(神奈川県のデータを引用)を見ると、新規感染者数は青の棒グラフで示されている。しかし、土日の検査が月曜に計上されたりするため、青の棒グラフは週ごとに増減があって分かりにくいため、移動平均の値が合わせてオレンジ色の折れ線グラフで表示されている。しかし、オレンジ色のグラフは、青のグラフより少し右にずれていると思いませんか?

これは、移動平均といっても過去7日間の平均をグラフ化しているため、数日分だけ右にずれているように見えている。ずれが無いように見せたいのなら、3日前から3日後のデータの移動平均であれば、ずれは無くなると思われる。

加重移動平均

過去の値を使った移動平均では遅れが発生する。でも、平均を取る際に、「n回前の値」と「現在の値」を考えた時、「その瞬間の平均値」は「現在の値」の方が近い値のはず。であれば、平均を取る時に、「n回前の値は少なめ」「現在の値は多め」に比重をかけて加算する方法がある。

for( int t = 3 ; t < SIZE ; t++ ) {
   // 数個の移動平均だし、
   // ループを使わずに書いてみる。 
   int s = x[t]   * 3   // 現在の値は大きい重み
         + x[t-1] * 2   // 1つ前の値
         + x[t-2] * 1 ; // 2つ前の値(重みは最小)
   y[t] = s / (3+2+1) ;
}

この様に、過去に遡るにつれ、平均をとる比重を直線的に小さくしながら移動平均をとる方法は、加重移動平均と呼ばれる。以下にその変化をExcelでシミュレーションしたものを示す。

指数移動平均

ここまで説明してきた、単純移動平均や、加重移動平均は、平均をとる範囲の「過去の値」を記憶しておく必要がある。広い時間にわたる移動平均をとる場合は、それに応じてメモリも必要となる。これは、組み込み型の小型コンピュータであれば、メモリが足りず平均処理ができない場合もでてくる。

そこで、荷重移動平均の重みを、は、100%,は50%,は25%… というように、過去に遡るにつれ、半分にして平均をとる。

しかし、以降の項で、 を使うと以下のように書き換えることができる。

// 指数移動平均は、プログラムがシンプル
//  1つ前の平均y[t-1]を覚えるだけでいい。
for( int t = 1 ; t < SIZE ; t++ ) {
   y[t] = ( x[t] + y[t-1] ) / 2 ;
}

この方法であれば、直前の平均値を記録しておくだけで良い。このような移動平均を、指数移動平均と呼ぶ。

ここで示した指数移動平均は、過去を遡るにつれとなっているが、これをさらに一般化した指数移動平均は、以下の式で示される。前述の移動平均は、とみなすことができる。

#define ALPHA 0.5
for( int t = 1 ; t < SIZE ; t++ ) {
    y[t] = ALPHA * x[t] + (1.0 - ALPHA) * y[t-1] ;
}

以下のプログラムは、うまく動かない。理由を説明せよ。

#define RVA 4
for( int t = 1 ; t < SIZE ; t++ ) {
   // 以下はy[t]は全部ゼロになる。
   y[t] = 1/RVA * x[t] + (1.0 - 1/RVA) * y[t-1] ;

   // 以下は、整数型演算だけで、正しく動くだろう。
   // y[t] = ( x[t] + (RVA-1) * y[t-1] ) / RVA ;
}

理解度確認のための小レポート

上記の移動平均の理解のために、以下の資料(講義では印刷資料を配布)の表の中を、電卓などを使って計算せよ。
計算したら、その結果をグラフの中にプロットし、どういった波形となるか確認し、レポートとして提出すること。

この課題は、こちらの Teams フォルダに提出してください。

UMLと振る舞い図

前回の講義で説明した構造図に続いて、処理の流れを説明するための振る舞い図の説明。

講義の後半は、UML作成のレポートの課題時間とする。

振る舞い図

参考資料をもとに振る舞い図の説明を行う。

ユースケース図

ユーザなど外部からの要求に対する、システムの振る舞いを表現するための活用事例や機能を表す図がユースケース図。 システムを構築する際に、最初に記述するUMLであり、システムに対する処理要件の全体像や機能を理解するために記述する。 ユーザや外部のシステムは、アクターとよび人形の絵で示す。楕円でシステムに対する具体的な処理をユースケースとして楕円で記述する。 関連する複数のユースケースをまとめて、サブジェクトとして示す場合もある。

上記の例は、学生が受講登録をして、授業に参加し、テストを受けるという様を表現したユースケース図である。また、下記の例にて、私自身が児童の保護システムを構築した際のユースケース図を示す。このように、システムの機能がどういったものがあるのかを網羅的に説明する際にユースケース図がよく使われる。

アクティビティ図

処理順序を記述するための図にはフローチャートがあるが、上から下に処理順序を記述するため、縦長の図になりやすい。また、四角枠の中に複雑なことを書けないので、UMLではアクティビティ図を用いる。

上記のアクティビティ図は、朝起きて出勤するまでの処理の流れを記述したものである。フローチャートと違い上から下に延びる図に限らず左右に広げて記載してある。

初期状態●から、終了状態◉までの手順を示すためのものがアクティビティ図。 フローチャートに無い表現として、複数の処理を並行処理する場合には、フォークノードで複数の処理を併記し、最終的に1つの処理になる部分をジョインノードで示す。 通常の処理は、角丸の長方形で示し、条件分岐(デシジョンノード)や合流(マージノード)はひし形で示す。

ステートチャート図(状態遷移図)

ステートチャート図は、処理内部での状態遷移を示すための図。 1つの状態を長丸長方形で示し、初期状態●から終了状態◉までを結ぶ。 1つの状態から、なんらかの状態で他の状態に遷移する場合は、分岐条件となる契機(タイミング)とその条件、およびその効果(出力)を「契機[条件]/効果」で矢印に併記する。 複数の状態をグループ化して表す場合もある。

上記のステートチャート図は、普通高校と高専の入学から卒業就職までを記載したものである。

シーケンス図

複数のオブジェクトが相互にやり取りをしながら処理が進むようなもののタイミングを記述するためのものがシーケンス図という。 上部の長方形にクラス/オブジェクトを示し、その下に縦軸にて時系列の処理の流れの線(Life Line)を描く。 オブジェクトがアクティブな状態は、縦長の長方形で示し、そのLife Line間を、やり取り(メッセージ)の線で相互に結ぶ。 メッセージは、相手側からの返答を待つような同期メッセージは、黒塗り三角矢印で示す。 返答を待たない非同期メッセージは矢印で示し、返答は破線で示す。

上のシーケンス図は、顧客が店員と対応しながらPOS端末でお金の出し入れをする様を表現したものとなっている。

コミュニケーション図

クラスやオブジェクトの間の処理とその応答(相互作用)と関連の両方を表現する図。

応答を待つ同期メッセージは -▶︎、非同期メッセージは→で表す。複数のオブジェクト間のやりとりの相互作用を表現する。

タイミング図

タイミング図は、クラスやオブジェクトの時間と共に状態がどのように遷移するのかを表現する図。

状態変化の発生するタイミングや、時間的な遅れや時間的な制約を図で明記するために使われる。

IT専科・UML入門より引用


UMLで人に説明する図の書き方として紹介してきたけど、よく現場で使われる図としては、ポンチ絵も名前だけは紹介したい。

ポンチ絵

ポンチ絵は、元々は風刺画のような漫画のことをであったが、最近ではビジネススの世界では「構想図」の意味で使われる。 製図の下書きとして作成するものや、イラストや図を使って概要をまとめた企画書などのことを言う。UMLのような書式のルールがある訳ではなく、相手に如何に印象付けるかが基本であり、ポンチ絵1つで企画の是非がきまったりもする。

# プレゼンで文字密度の高いポンチ絵で説明されると、時として細かい所が読めずにイライラすることもある。

製図の下書きとしてのポンチ絵

イラストや図で概要をまとめた企画書としてのポンチ絵

シェルスクリプトの演習

今回は、前回までのシェルの機能を使って演習を行う。

プログラムの編集について

演習用のサーバに接続して、シェルスクリプトなどのプログラムを作成する際のプログラムの編集方法にはいくつかの方式がある。

  • サーバに接続しているターミナルで編集
    • nano , vim , emacs などのエディタで編集
  • パソコンで編集してアップロード
    • scp 命令で編集したファイルをアップロード
  • パソコンのエディタのリモートファイルの編集プラグインで編集
    • VSCode の remote-ssh プラグインを使うのが簡単だけど、サーバ側の負担が大きいので今回は NG

リモート接続してエディタで編集

今回の説明では、emacs で編集する方法を説明する。

((( Emacs を起動 )))
guest00@nitfcei.mydns.jp:~$ emacs helloworld.sh

エディタが起動すると、以下のような画面となる。

scpでファイルをアップロード

scpコマンドは、ssh のプロトコルを使ってネットワークの先のコンピュータとファイルのコピーを行う。前述の emacs などのエディタが使いにくいのなら scp を使えばいい。

((( scp 命令の使い方 )))
$ scp ユーザ名@ホスト名:ファイルの場所 

((( サーバの helloworld.sh をダウンロード )))
C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:helloworld.sh .
C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:/home0/Challenge/3-shellscript/helloworld.sh .
((( パソコンの hoge.sh をアップロード )))
C:\Users\t-saitoh> scp -P 443 hoge.sh guest00@nitfcei.mydns.jp:
((( パソコンの hoge.html を public_html にアップロード ))) 
C:\Users\t-saitoh> scp -P 443 hoge.html guest00@nitfcei.mydns.jp:public_html

シェルスクリプトの命令

条件式の書き方

シェルには、test コマンド( [ コマンド ) で条件判定を行う。動作の例として、テストコマンドの結果を コマンドの成功/失敗 を表す $? を使って例示する。

guest00@nitfcei:~$ [ -f helloworld.sh ] ; echo $?    # [ -f ファイル名 ]
0                                                    # ファイルがあれば0/なければ1
guest00@nitfcei:~$ [ -x /bin/bash ]; echo $?         # [ -x ファイル名 ]
0                                                    # ファイルが存在して実行可能なら0/だめなら1
guest00@nitfcei:~$ [ -d /opt/local/bin ] ; echo $?   # [ -d ディレクトリ名 ]
1                                                    # ディレクトリがあれば0/なければ1
guest00@nitfcei:~$ [ "$PATH" = "/bin:/usr/bin" ] ; echo $?   # [ "$変数" = "文字列" ]
1                                                    # $変数が"文字列"と同じなら0/違えば1

シェルの制御構文

((( シェルの if 文 )))
if [ -f helloworld.sh ]; then
   echo "exist - helloworld.sh"
elif [ -f average.c ]; then
   echo "exist - average.c"
else
   echo "みつからない"
fi
((( シェルの for 文 )))
for user in /home0/guests/*   # ワイルドカード文字 * があるので、/home0/guests/ のファイル一覧
do                            # が取り出されて、その1つづつが、$user に代入されながら繰り返し。
    echo $user
done
---
結果: /home0/guests/guest00, /home0/guests/guest01 ... 
((( while 文 )))
/bin/grep ^guest < /etc/passwd \    # passwd ファイルでguestで始まる行を抜き出し、
| while read user                   # read コマンドで その 行データを $user に代入しながらループ
  do
      echo $user
  done

シェル演習向けのコマンド一例

`コマンド`と$(コマンド)

((( コマンドの結果を使う )))
guest00@nitfcei:~$ ans=`whoami`     # whoami コマンドの結果を ans に代入
guest00@nitfcei:~$ echo $ans        # バッククオートに注意 ' シングルクオート " ダブルクオート ` バッククオート
guest00
guest00@nitfcei:~$ ans=$(pwd)       # pwd コマンドの結果を ans に代入
guest00@nitfcei:~$ echo $ans        # 最近は、$(コマンド) の方が良く使われている
/home0/guest00

コマンドライン引数

シェルの中でコマンドライン引数を参照する場合には、”$数字“, “$@” を使う。$1 , $2 で最初のコマンドライン引数, 2番目のコマンドライン引数を参照できる。すべてのコマンドライン引数を参照する場合には、$@ を使う。

((( argv.sh : コマンドライン引数を表示 )))
#!/bin/bash
echo "$@"
for argv in "$@"
do
    echo "$argv"
done
((( argv.sh を実行 )))
guest00@nitfcei:~$ chmod 755 argv.sh
guest00@nitfcei:~$ ./argv.sh abc 111 def
abc 111 def          # echo "$@" の結果
abc                  # for argv ... の結果
111
def

cutコマンドとawkコマンド

((( 行の特定部分を抜き出す )))
guest00@nitfcei:~$ cut -d: -f 1 /etc/passwd   # -d:  フィールドの区切り文字を : で切り抜き
root                                          # -f 1 第1フィールドだけを出力
daemon
adm
:
guest00@nitfcei:~$ awk -F: '{print $1}' /etc/passwd  # -F: フィールド区切り文字を : で切り分け
root                                                 # ''
daemon
adm
:

lastコマンド

((( ログイン履歴を確認 )))
guest00@nitfcei:~$ last
t-saitoh pts/1        64.33.3.150      Thu Jul  7 12:32   still logged in
最近のログインした名前とIPアドレスの一覧
:
((( guest* がログインした履歴 )))
guest00@nitfcei:~$ last | grep guest
guest15  pts/11       192.156.145.1    Tue Jul  5 16:00 - 16:21  (00:21)
:
((( 7/5にログインしたguestで、名前だけを取り出し、並び替えて、重複削除 )))
guest00@nitfcei:~$ last | grep guest | grep "Jul  5" | awk '{print $1}' | sort | uniq
7/5("Jul  5")の授業で演習に参加していた学生さんの一覧が取り出せる。
### あれ、かなりの抜けがあるな!?!? ###

whoisコマンド

((( IPアドレスなどの情報を調べる )))
guest00@nitfcei:~$ whois 192.156.145.1
:
inetnum:        192.156.145.0 - 192.156.148.255
netname:        FUKUI-NCT
country:        JP
:
guest00@nitfcei:~$ whois 192.156.145.1 | grep netname:
netname:   FUKUI-NCT
netname:   ANCT-CIDR-BLK-JP

シェルスクリプトのセキュリティ

ここまでのプログラムの動作例では、a.out などのプログラムを実行する際には、先頭に “./” をつけて起動(./a.out)している。これは「このフォルダ(“./“)にある a.out を実行せよ」との意味となる。

いちいち、カレントフォルダ(“./”)を先頭に付けるのが面倒であっても、環境変数 PATH を “export PATH=.:/bin:/usr/bin” などと設定してはいけない。こういった PATH にすれば、”a.out” と打つだけでプログラムを実行できる。しかし、”ls” といったファイル名のプログラムを保存しておき、そのフォルダの内容を確認しようとした他の人が “ls” と打つと、そのフォルダの中身を実行してしまう。

guest00@nitfcei:~$ export PATH=".:/bin:/bin/bash"
guest00@nitfcei:~$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls
#!/bin/bash

killall -KILL bash
guest00@nitfcei:~$ cd /home0/Challenge/1-CTF.d/Task5/Bomb
guest00@nitfcei:~$ ls
# 接続が切れる(bashが強制停止となったため)

こういったシェルスクリプトでのセキュリティのトラブルを防ぐために、

  • 環境変数PATHに、カレントフォルダ”./”を入れない
  • シェルスクリプトで外部コマンドを記述する際には、コマンドのPATHをすべて記載する。
    コマンドのPATHは、which コマンドで確認できる。echo とか [ といったコマンドは、bash の組み込み機能なので、コマンドのPATHは書かなくていい。

演習問題

シェルスクリプトの練習として、以下の条件を満たすものを作成し、スクリプトの内容の説明, 機能, 実行結果, 考察を記載したワードファイル(or PDF)等で、こちらのフォルダに提出してください。

  • スクリプトとして起動して結果が表示されること。(シバン,実行権限)
  • コマンドライン引数を使っていること。
  • 入出力リダイレクトやパイプなどを使っていること。
  • 以下の例を参考に。
((( 第1コマンドライン引数指定したユーザが、福井高専からアクセスした履歴を出力する。)))
#!/bin/bash

if [ -x /usr/bin/last -a -x /bin/grep ]; then   # [ ... -a ... ] は、複数条件のAND
    /usr/bin/last "$1" | /bin/grep 192.156.14
fi
-------------------------------------------------------------------------
((( guest グループで、$HOME/helloworld.sh のファイルの有無をチェック )))
#!/bin/bash

for dir in /home0/guests/*
do
   if [ -f "$dir/helloworld.sh" ]; then      # PATHの最後の部分を取り出す
      echo "$(/usr/bin/basename $dir)"       # $ basename /home0/guests/guest00
   fi                                        # guest00                  ~~~~~~~basename
done

スタックと待ち行列

前回の授業では、リストの先頭にデータを挿入する処理と、末尾に追加する処理について説明したが、この応用について説明する。

計算処理中に一時的なデータの保存として、スタック(stack)待ち行列・キュー(queue)がよく利用される。それを配列を使って記述したり、任意の大きさにできるリストを用いて記述することを示す。

スタック

配列を用いたスタック

一時的な値の記憶によく利用されるスタック(stack)は、データの覚え方の特徴からLIFO( Last In First out )とも呼ばれる。配列を使って記述すると以下のようになるであろう。

import java.util.*;

public class Main {
    static final int STACK_SIZE = 10 ;
    static int[] stack = new int[ STACK_SIZE ] ;
    static int   sp    = 0 ;
    static void push( int x ) {
        stack[ sp++ ] = x ;
    }
    static int pop() {
        return stack[ --sp ] ;
    }
    public static void main(String[] args) throws Exception {
        push( 11 ) ;
        push( 22 ) ;
        push( 33 ) ;
        System.out.println( pop() ) ;
        System.out.println( pop() ) ;
        System.out.println( pop() ) ;
    }
}

配列を使った Stack をオブジェクト指向で記述するなら、以下のように書ける。

import java.util.*;

class Stack {
    static final int STACK_SIZE = 10 ;
    int[] array ;
    int   sp ;
    Stack() {
        this.array = new int[ STACK_SIZE ] ;
        this.sp    = 0 ;
    }
    void push( int x ) {
        array[ sp++ ] = x ;
    }
    int pop() {
        return array[ --sp ] ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        Stack stack = new Stack() ;
        stack.push( 11 ) ;
        stack.push( 22 ) ;
        stack.push( 33 ) ;
        System.out.println( stack.pop() ) ;
        System.out.println( stack.pop() ) ;
        System.out.println( stack.pop() ) ;
    }
}

C言語で書いた場合

#define STACK_SIZE 32
int stack[ STACK_SIZE ] ;
int sp = 0 ;

void push( int x ) { // データをスタックの一番上に積む
    stack[ sp++ ] = x ;
}
int pop() { // スタックの一番うえのデータを取り出す
    return stack[ --sp ] ;
}
void main() {
    push( 1 ) ; push( 2 ) ; push( 3 ) ;
    printf( "%d\n" , pop() ) ; // 3
    printf( "%d\n" , pop() ) ; // 2
    printf( "%d\n" , pop() ) ; // 1
}

++,–の前置型と後置型の違い

// 後置インクリメント演算子
int i = 100 ;
printf( "%d" , i++ ) ;
// これは、
printf( "%d" , i ) ;
i++ ;
// と同じ。100が表示された後、101になる。

// 前置インクリメント演算子
int i = 100 ;
printf( "%d" , ++i ) ;
//   これは、
i++ ;
printf( "%d" , i ) ;
// と同じ。101になった後、101を表示。

リスト構造を用いたスタック

しかし、この中にSTACK_SIZE以上のデータは貯えられない。同じ処理をリストを使って記述すれば、配列サイズの上限を気にすることなく使うことができるだろう。では、リスト構造を使ってスタックの処理を記述してみる。

import java.util.*;

class ListNode {
    int      data ;
    ListNode next ;
    ListNode( int d , ListNode n ) {
        this.data = d ;
        this.next = n ;
    }
}

public class Main {
    static ListNode stack = null ;
    static void push( int x ) {
        stack = new ListNode( x , stack ) ;
    }
    static int pop() {
        int ans = stack.data ;
        stack = stack.next ;
        return ans ;
    }
    public static void main(String[] args) throws Exception {
        push( 1 ) ;
        push( 2 ) ;
        push( 3 ) ;
        System.out.println( pop() ) ;
        System.out.println( pop() ) ;
        System.out.println( pop() ) ;
    }
}
struct List* stack = NULL ;

void push( int x ) { // リスト先頭に挿入
    stack = cons( x , stack ) ;
}
int pop() { // リスト先頭を取り出す
    int ans = stack->data ;
    struct List* d = stack ;
    stack = stack->next ;      // データ 0 件で pop() した場合のエラー対策は省略
    free( d ) ;
    return ans ;
}

オブジェクト指向っぽく書くならば、下記のようになるだろう。初期状態で stack = null にしておくと、stack.push() ができないので、stack の先頭には、ダミーデータを入れるようにプログラムを書くと以下のようになるだろう。

import java.util.*;

class ListNode {
    int      data ;
    ListNode next ;
    ListNode( int d , ListNode n ) {
        this.data = d ;
        this.next = n ;
    }
    ListNode() {   // stack初期化用のコンストラクタ
        this.data = -1 ;
        this.next = null ;
    }
    void push( int x ) {
        this.next = new ListNode( x , this.next ) ;
    }
    int pop() {
        int ans = this.next.data ;
        this.next = this.next.next ;
        return ans ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        ListNode stack = new ListNode() ; // stack初期化用のコンストラクタを使う
        stack.push( 1 ) ;
        stack.push( 2 ) ;
        System.out.println( stack.pop() ) ;
        System.out.println( stack.pop() ) ;
    }
}

キュー(QUEUE)

2つの処理の間でデータを受け渡す際に、その間に入って一時的にデータを蓄えるためには、待ち行列(キュー:queue)がよく利用される。 データの覚え方の特徴からFIFO(First In First Out)とも呼ばれる。

配列を用いたQUEUE / リングバッファ

配列にデータを入れる場所(wp)と取り出す場所のポインタ(rp)を使って蓄えれば良いが、配列サイズを超えることができないので、データを取り出したあとの場所を循環して用いるリングバッファは以下のようなコードで示される。

import java.util.*;

public class Main {
    static final int QUEUE_SIZE = 32 ;
    static int[] queue = new int[ QUEUE_SIZE ] ;
    static int wp = 0 ;
    static int rp = 0 ;
    static void put( int x ) {
        queue[ wp++ ] = x ;
        if ( wp >= QUEUE_SIZE ) // wp = wp % QUEUE_SIZE ; or wp = wp & (QUEUE_SIZE - 1) ;
            wp = 0 ;
    }
    static int get() {
        int ans = queue[ rp++ ] ;
        if ( rp >= QUEUE_SIZE ) // rp = rp % QUEUE_SIZE ; or rp = rp & (QUEUE_SIZE - 1) ;
            rp = 0 ;
        return ans ;
    }
    public static void main(String[] args) throws Exception {
        // Your code here!
        put( 1 ) ;
        put( 2 ) ;
        put( 3 ) ;
        System.out.println( get() ) ;
        System.out.println( get() ) ;
        System.out.println( get() ) ;
    }
}
#define QUEUE_SIZE 32
int queue[ QUEUE_SIZE ] ;
int wp = 0 ; // write pointer(書き込み用)
int rp = 0 ; // read  pointer(読み出し用)

void put( int x ) { // 書き込んで後ろ(次)に移動
    queue[ wp++ ] = x ;
    if ( wp >= QUEUE_SIZE )  // 末尾なら先頭に戻る
        wp = 0 ;
}
int get() { // 読み出して後ろ(次)に移動
    int ans = queue[ rp++ ] ;
    if ( rp >= QUEUE_SIZE )  // 末尾なら先頭に戻る
        rp = 0 ;
    return ans ;
}
void main() {
    put( 1 ) ; put( 2 ) ; put( 3 ) ;
    printf( "%d\n" , get() ) ; // 1
    printf( "%d\n" , get() ) ; // 2
    printf( "%d\n" , get() ) ; // 3
}

このようなデータ構造も、get() の実行が滞るようであれば、wp が rp に循環して追いついてしまう。このため、上記コードはまだエラー対策としては不十分である。どのようにすべきか?

 

リスト構造を用いたQUEUE

前述のリングバッファもget()しないまま、配列上限を越えてput()を続けることはできない。

この配列サイズの上限問題を解決したいのであれば、リスト構造を使って解決することもできる。この場合のプログラムは、以下のようになるだろう。

import java.util.*;

class ListNode {
   int      data ;
   ListNode next ;
   ListNode( int d , ListNode n ) {
       this.data = d ;
       this.next = n ;
   }
} ;

public class Main {
    static ListNode top = new ListNode( -1 , null ) ;
    static ListNode tail = top ;
    static void put( int x ) {
        tail.next = new ListNode( x , null ) ;
        tail = tail.next ;
    }
    static int get() {
        int ans = top.next.data ;
        top.next = top.next.next ;
        return ans ;
    }
    public static void main(String[] args) throws Exception {
        put( 1 ) ;
        put( 2 ) ;
        put( 3 ) ;
        System.out.println( get() ) ;
        System.out.println( get() ) ;
        System.out.println( get() ) ;
    }
}

Javaで書かれた ListNode を用いた待ち行列のイメージ図は下記のように示される。

struct List* queue = NULL ;
struct List** tail = &queue ;

void put( int x ) { // リスト末尾に追加
    *tail = cons( x , NULL ) ;
    tail = &( (*tail)->next ) ;
}
int get() { // リスト先頭から取り出す
    int ans = queue->data ;
    struct List* d = queue ;
    queue = queue->next ;
    free( d ) ;
    return ans ;
}

ただし、上記のプログラムは、データ格納後にget()で全データを取り出してしまうと、tail ポインタが正しい位置になっていないため、おかしな状態になってしまう。
また、このプログラムでは、rp,wp の2つのポインタで管理することになるが、 2重管理を防ぐために、リストの先頭と末尾を1つのセルで管理する循環リストが使われることが多い。

理解確認

  • 配列を用いたスタック・待ち行列は、どのような処理か?図などを用いて説明せよ。
  • リスト構造を用いたスタック・待ち行列について、図などを用いて説明せよ。
  • スタックや待ち行列を、配列でなくリスト構造を用いることで、どういう利点があるか?欠点があるか説明せよ。
  • 配列を用いたリングバッファが用いられている身近な例にはどのようなものがあるか?
  • 配列を用いたリングバッファを実装する場合配列サイズには 2n 個を用いることが多いのはなぜだろうか?

UMLと構造図

UMLの構造図の書き方の説明。 詳しくは、参考ページのUML入門などが、分かりやすい。

クラス図

クラス図は、構造図の中の基本的な図で、 枠の中に、上段:クラス名、中段:属性(要素)、下段:メソッド(関数)を記載する。 属性やメソッドの可視性を示す場合は、”-“:private、”+”:public、”#”:protected 可視性に応じて、”+-#”などを記載する。

関連

クラスが他のクラスと関係がある場合には、その関係の意味に応じて、直線や矢印で結ぶ。
(a)関連(association):単純に関係がある場合、
(b)集約(aggregation):部品として持つが、弱い結びつき。関係先が消滅しても別に存在可能。(has-a)
(c)コンポジション(composition):部品として持つが強い結びつき。関係先と一緒に消滅。(has-a)
(d)依存(dependency):依存関係にあるだけ
(e)派生(generalization):派生・継承した関係(is-a)
(f)実現(realization): Javaでのinterfaceによる多重継承

上図の例では、乗り物クラスVehicleから自動車Carが派生し(CarからVehicleへの三角矢印―▷)、 自動車は、エンジン(Engine)を部品として持つ(EngineからCarへのひし形矢印―◆)。エンジンは車体と一緒に廃棄なら、コンポジション(C++であれば部品の実体を持つ)で実装する。

自動車は、同じく車輪(Wheel)を4つ持つが、自動車を廃棄してもタイヤは別に使うかもしれないので、集約(部品への参照を持つ)で実装する(WheelからCarへのひし形矢印―◇)。 集約で実装する場合は、C++などであれば、ポインタで部品を持ち、部品の廃棄(delete)は、別に行うことになる。

Javaなどのプログラム言語では、オブジェクトはデータの実体へのポインタで扱われるため、コンポジションと集約を区別して表現することは少ない。

is-a 、has-a の関係

前の課題でのカモノハシクラスで、羽や足の情報をどう扱うべきかで、悩んだ場合と同じように、 クラスの設計を行う場合には、部品として持つのか、継承として機能を持つのか悩む場合がある。 この場合には、“is-a”の関係“has-a”の関係で考えると、部品なのか継承なのか判断しやすい。

たとえば、上の乗り物(Vehicle)クラスと、車(Car)のクラスは、”Car is-a Vehicle” といえるので、is-a の関係。 “Car is-a Engine”と表現すると、おかしいことが判る。 車(Car)とエンジン(Engine)のクラスは、”Car has-a Engine”といえるので、has-a の関係となる。 このことから、CarはVehicleからの派生であり、Carの属性としてEngineを部品として持つ設計となる。

ER図

UMLではないが、オブジェクト図に近いものとしてER図がある。これはリレーショナルデータベースの設計が正しいか確認しながら設計するための図で、Entity(実体)とRelation(関連)を相互に線で結んだもので、最近のER図の書き方は、かなりクラス図の書き方に似ている。

オブジェクト図

クラス図だけで表現すると、複雑なクラス関係では、イメージが分かりづらい場合がでてくる。 この場合、具体的な値を図に書き込んだオブジェクトで表現すると、説明がしやすい場合がある。 このように具体的な値で記述するクラス図は、オブジェクト図と言う。 書き方としては、クラス名の下に下線を引き中段の属性の所には具体的な値を書き込んで示す。

その他の構造図

パッケージ図

パッケージ図は、クラス図をパッケージ毎に分類して記載する図。 パッケージのグループを、フォルダのような図で記載する。


IT専科から引用

コンポーネント図とコンポジット構造図

コンポジット構造図は、クラスやコンポーネントの内部構造を示すもので、コンポーネント図は、複数のクラスで構成される処理に、 インタフェースを用意し、あたかも1つのクラスのように扱ったもの。 接続するインタフェースを飴玉と飴玉を受けるクチのイメージで、提供側を◯───で表し、要求側を⊃──で表す。


IT専科から引用

配置図

配置図は、システムのハードウェア構成や通信経路などを表現するための図。 ハードウェアは直方体の絵で表現し、 デバイスの説明は、”≪device≫”などを示し、実行環境には、”≪executionEnvironment≫” などの目印で表現する。


IT専科から引用

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー