背景
前編では、自動給餌システムにおけるSPLEの可変性の分析、システム構成、及びC言語で実現する方法について説明しました。
後編では、実際に変異体を置き換えたときの効果、嬉しさについて説明したいと思います。
やりたいこと
給餌時間を表示するためのLCDを入れ替える。
これは可変点として定義している範囲であるため、簡単に入れ替えることができます。
このように、製品の変わるところをあらかじめ定義しておき、変更しても簡単に製品を構築できる開発をプロダクトライン開発といい、ソフトウェアに適用したのがSPLE(ソフトウェアプロダクトライン開発)になります。
その製品における将来の拡張性や、市場の動向/ニーズなどを踏まえて、どう可変点を定義し、設計に組み込んでおくのかが重要になります。
実装
変更点概要:
- 前編で説明したディスプレイのインターフェースを継承した、LCDクラス(具象クラス)を作成
- メイン処理のインスタンス生成部を修正
1. 1602 LCDクラス(ディスプレイクラスを継承した具象クラス)(新規)
#ifndef ___DISPLAY_1602_LCD_H___
#define ___DISPLAY_1602_LCD_H___
//インターフェースの実装部のヘッダ
#include "InterFaceDisplay.h"
static void Display_OLCD(IDisplay* const p_this, int display_mode);
typedef struct display_1602_lcd
{
// インターフェース型の変数をクラスの先頭に定義
IDisplay interface;
// 具象クラスなので、メンバ変数も定義可能
int private_display_mode;
}Display_1602_lcd;
// 公開関数定義
void Display_1602_LCD_construct(IDisplay* const p_this);
#endif
#include "Display_1602_LCD.h"
#include <LiquidCrystal_I2C.h>
//アドレス0x27 16文字2行の液晶
static LiquidCrystal_I2C lcd(0x27, 16, 2);
// インターフェース実装本体
// 第一引数にはインターフェースを実装したインスタンスのポインタを指定
static void Display_1602_LCD(IDisplay* const p_this, int display_mode, int display_time){
Display_1602_lcd* const p = (Display_1602_lcd* const)p_this;
int hours;
int minites;
int seconds;
switch(display_mode){
case DISPLAY_MODE_INITIAL:
break;
case DISPLAY_MODE_SETTING_MODE:
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SettingTime=" + String(display_time) +" h");
break;
case DISPLAY_MODE_COUNT_MODE:
hours = (display_time / 60) / 60;
minites = (display_time / 60) % 60;
seconds = display_time % 60;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("restTime :");
lcd.setCursor(0, 1);
lcd.print(String(hours) + "h");
lcd.print(String(minites) + "m");
lcd.print(String(seconds) + "s");
break;
case DISPLAY_MODE_FEED:
break;
}
}
// Feedクラスのインターフェース実装へアクセスするための関数テーブル
static const IDisplayMethod DISPLAY_OLCD_METHODS = {
Display_1602_LCD,
};
// 公開関数定義
void Display_1602_LCD_construct( IDisplay* const p_this){
// インターフェースの関数テーブルのポインタ初期化
// インターフェース定義から実装本体へのアクセスが設定できる
((IDisplay*)p_this)->p_methods = &DISPLAY_OLCD_METHODS;
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("FEEDSYSTEM");
lcd.setCursor(0, 1);
lcd.print("1602 LCD ver");
}
2. メイン処理
ここでは、差分に注目してください。
インスタンス生成部と、コンストラクタの箇所だけ修正していて、他は修正していません。
※ボタン押されたときにどう表示するか、などは変わらないので。
これだけで、LCDを入れ替えた製品が構築できます。すごくないですか?
#include "InterfaceFeed.h"
#include "InterfaceDisplay.h"
- #include "Display_OLCD.h"
+ #include "Display_1602_LCD.h"
#include "button.h"
#include "Feed_cat.h"
#include "Feed_fish.h"
#define FEEDSYSTEM_SELECT 0x00
#define FEEDSYSTEM_ENTER 0x01
#define FEEDSYSTEM_TIMER 0x02
#define FEEDSYSTEM_FEED 0x03
#define FEEDSYSTEM_END 0x04
#define TIMER_UNELAPSED 0x00
#define TIMER_ELAPSED 0x01
// 変数
static unsigned long SettingTime = 0;
static unsigned long PreSettingTime = 0;
static int FeedSystem_mode = 0;
static int Timer_limit_time = 0;
static unsigned long Timer_setting_time = 0;
// インスタンス
IFeed feed_instance;
- Display_OLCD Display_instance;
+ Display_1602_lcd Display_instance;
static void Feed_processing(IFeed* const p_feed);
static void Timer_settime(unsigned long SettingTime);
static unsigned long Timer_judge_elapsed(unsigned long* io_SettingTime);
void setup() {
//インスタンス初期化
- Display_construct((IDisplay*)&Display_instance);
+ Display_1602_LCD_construct((IDisplay*)&Display_instance);
Feed_construct((IFeed*)&feed_instance);
button_init();
}
void loop() {
int ret_buttonread;
int judge_elapsed_time;
// pin14, からボタン入力取得
ret_buttonread = buttonread();
switch(FeedSystem_mode){
case FEEDSYSTEM_SELECT:
if (ret_buttonread == SELECT_BUTTON){
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_SETTING_MODE, SettingTime);
SettingTime++;
}else if(ret_buttonread == ENTER_BUTTON){
FeedSystem_mode = FEEDSYSTEM_ENTER;
}
break;
case FEEDSYSTEM_ENTER:
SettingTime--;//時間調整
Timer_settime(SettingTime);
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_COUNT_MODE, SettingTime);
FeedSystem_mode = FEEDSYSTEM_TIMER;
break;
case FEEDSYSTEM_TIMER:
judge_elapsed_time = Timer_judge_elapsed(&SettingTime);
if (PreSettingTime != SettingTime){
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_COUNT_MODE, SettingTime);
PreSettingTime = SettingTime;
}
if (judge_elapsed_time == TIMER_ELAPSED){
FeedSystem_mode = FEEDSYSTEM_FEED;
}
break;
case FEEDSYSTEM_FEED:
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_FEED, SettingTime);
Feed_processing((IFeed*)&feed_instance );
FeedSystem_mode = FEEDSYSTEM_END;
break;
default:
break;
}
}
static void Feed_processing(IFeed* const p_feed ){
p_feed->p_methods->Feed( p_feed );
}
static void Display_processing(IDisplay* const p_display, const int display_mode, const int display_Time){
p_display->p_methods->Display( p_display, display_mode, display_Time);
}
return ret;
}
結果
前編にOLEDで表示したのが以下になります。
(ボタンを押す度に1時間ずつ追加、長押しするとカウントダウン開始)
以下が、今回変更して表示した結果です。
嬉しさ、結論
今回の嬉しさとしては、
ボタンを押して時間を1hずつ増やして、長押しするという制御は変わらないので一切手を入れず、
違うバリエーションの製品を作れたことです。
あくまで、変更したところはLCDを変更したことによる表示の制御部分と、インスタンス生成部だけです。
このような想定している変更点を整理し(可変性分析)、
それが変わったとしてもソフトの変更量を最小限にできる設計をし、
共通部品(「コア資産」)を増やしていくことで、
似たような製品を低コストで開発可能、
製品管理効率向上(製品に共通する処理の変更は、コア資産を変更することで一律修正可能)などのメリットがあります。
コピペを繰り返し、製品毎にいろんな変更を加えていると、どの変更がどの製品に影響があるのか、分析しきれなくなり、結局一から作り直す羽目になります。
たとえば今回の例で、カウントダウン開始は、ボタンを長押しではなく、別の決定ボタンを追加してそちらを押したときに変更したい場合は、「コア資産」をアップデートしていけばよく、製品毎に変更を管理する必要はありません。
製品毎に管理するのはあくまで製品毎の違いを実装する部分(今回の例だとLCD制御部)のみです。
これらの開発経験がなかったので、今回自動給餌システムを開発して、疑似体験してみました。
どなたかの何かの参考になれば嬉しいです。