技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

問題は分割して考えるべき

一般的に、何かの問題を解決したい場合は、解決する道筋を考えて、問題を分割しようとするべきです。
そうすることで、
・やるべきことが明確になる
・わからない所が出てきた時に調べやすくなる
・作業分担ができるようになる
といったメリットがあります。
 
これは、プログラミングにも言えることです。
初学者の場合、何かのプログラムを作る時に「何から手をつけたら良いのかわからない」という状態になることがあります。
この状態になった時は、どのように処理を組み合わせれば作りたいものを作れそうか、日本語で良いので列挙することが重要です。
そうすることで、思考を整理できますし、熟練者に質問もしやすくなります。
 
----
 
例として
Windows端末で、ワンタッチでファイル内の"a"の文字を"b"に置換する」
という問題を考えてみます。
 
まずは、以下のような2つの問題に分割することができます。
・どのプログラミング言語で実装するか
・どのようなロジックで実装するか
 
----
 
「どのプログラミング言語で実装するか」という問題に関しては、今回は手軽さ・性能の高さ・汎用性の高さのバランスが良いC#で実装することにします。
 
「どのようなロジックで実装するか」という問題については、更に以下の3つの問題に分割することができます。
C#のプログラムをワンタッチで実行する
・ファイルの入出力を行う
・"a"の文字を"b"に置換する
 
----
 
C#のプログラムをワンタッチで実行する」については、「Hello World!」と呼ばれる文字を出力するだけの最も簡単なプログラムを実行してみるのが良いです。
今回は、以下のように、batファイルから実行する方法を採用します。これにより、batファイルをダブルクリックするだけでC#のプログラムを動かすことができるようになります。
 
・フォルダ構成
execute.bat
replace.cs
 
・execute.bat
@echo off

rem プログラムのコンパイル
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe replace.cs

rem プログラムの実行
replace.exe

rem コンパイルで出来上がった実行ファイルの削除
del replace.exe

pause
 
・replace.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Program
{
    class Program
    {
        static void Main(string args)
        {
            Console.WriteLine("Hello World!");
            Console.ReadKey(true);
        }
    }
}
 
・実行結果
コンソールが立ち上がり、"Hello World!"と出力される。
 
----
 
「ファイルの入出力を行う」については、ファイルを入力して1バイトずつ読み込んでそのまま出力するだけのプログラムを作るのが良いです。
先に作ったプログラムを少し改変してみます。
 
・フォルダ構成
execute.bat
replace.cs
files─input.txt
 
・replace.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Program
{
    class Program
    {
        static void Main(string args)
        {
            // 入力ファイルオープン
            BinaryReader br = new BinaryReader
                (new FileStream(@"files\input.txt", FileMode.Open));

            // 出力ファイルオープン
            BinaryWriter bw = new BinaryWriter
                (new FileStream(@"files\output.txt", FileMode.Create));
                
            // 読み書きの処理
            try
            {
                for (;;)
                {
                    // 1バイト読み込む
                    byte data = br.ReadByte();
                    
                    // 読み込んだ文字を出力する
                    bw.Write(data);
                }
            }
            
            // 最後まで読んだらループを抜ける
            catch(EndOfStreamException)
            {
            }
            
            // ファイルクローズ
            finally
            {
                br.Close();
                bw.Close();
            }
        }
    }
}
 
・input.txt
hogefugapiyo
 
・実行結果
filesフォルダの下にoutput.txtが生成され、中身はinput.txtと同じになる。
 
----
 
あとは、読み込んだ文字について「"a"の文字を"b"に置換する」処理を追加すれば、今回の問題は解決します。
先ほどのプログラムを改変してみます。
 
・replace.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Program
{
    class Program
    {
        static void Main(string[] args)
        {
            // 入力ファイルオープン
            BinaryReader br = new BinaryReader
                (new FileStream(@"files\input.txt", FileMode.Open));

            // 出力ファイルオープン
            BinaryWriter bw = new BinaryWriter
                (new FileStream(@"files\output.txt", FileMode.Create));
                
            // 読み書きの処理
            const byte Byte_a = (byte)0x61; // "a"の文字コード
            const byte Byte_b = (byte)0x62; // "b"の文字コード
            try
            {
                for (;;)
                {
                    // 1バイト読み込む
                    byte data = br.ReadByte();
                    
                    // 読み込んだ文字が"a"の場合は"b"に置き換え
                    if (data == Byte_a)
                    {
                        data = Byte_b;
                    }
                    
                    // 読み込んだ文字を出力する
                    bw.Write(data);
                }
            }
            
            // 最後まで読んだらループを抜ける
            catch(EndOfStreamException)
            {
            }
            
            // ファイルクローズ
            finally
            {
                br.Close();
                bw.Close();
            }
        }
    }
}
 
・実行結果
filesフォルダの下にoutput.txtが生成され、中身は以下の通りになる。
hogefugbpiyo

一時的なエスケープ文字としてバイナリ文字を使う

ファイルで文字列置換を行う際、特定の条件を満たす文字を置換したくない場合があります。
例えば、CSVファイルの区切り文字であるカンマをタブ文字に置換したい場合、文字列項目中に出現する区切り文字ではないカンマ、具体的に言うと""で囲まれた箇所のカンマは置換したくない場合があります。
その場合、置換したくないカンマを一時的にエスケープ文字に入れ替えた後、カンマを一括置換し、エスケープ文字を元の文字に戻すと、上手く置換できます。
 
例えば、以下のレコードをサクラエディタで置換したい場合、
5,"詰め合わせセット(ジュース,お菓子)",1000
正規表現をONにして以下のように置換すると上手くいきます。
 
置換前 ("[^,]*),([^,]*")
置換後 ${1}@${2}

置換前 ,
置換後 \t

置換前 @
置換後 ,
 
ここで、エスケープ文字としてどのような文字を使うかが問題になります。
上記の例では@をエスケープ文字としていましたが、ファイル中に@の文字が出現する場合、エスケープ文字をカンマに戻す際にその文字も一緒にカンマに置換されてしまいます。
これを回避するために、エスケープ文字として、通常入力されることがない文字を使用することが有効になります。
サクラエディタでは\x{01}のように指定すると16進コードで文字を指定できるため、これを利用して、キーボードから入力することが困難な文字を置換で指定することができます。
 
通常入力されることがないエスケープ文字として、以下の文字を使うことをお勧めします。
何れも、ASCIIコードで定義された現在では使われることが考えにくい制御文字であり、かつカナ拡張されたIBMEBCDICでも使われることがない文字です。
バイナリ文字が埋め込まれたファイルであったり、特殊な仕様が存在するファイルであったりしない限りは、使われる心配はしなくて良いでしょう。
 
・\x{01}~\x{08}
・\x{0b}~\x{0c}
・\x{0e}~\x{1f}

java:参照型変数の中身のイメージ

Javaの参照型変数の中身は、何年かJavaの実装経験を積んだ人でもイメージすることが難しいです。
しかし、ここがイメージできていないと、思わぬ落とし穴にはまることもあります。
 
C言語を経験していればイメージしやすくなりますが、そのためだけにC言語を学ぶのもハードルが高く感じると思います。
そこで、今回の記事で、参照型変数の中身のイメージについて、重要な点のみをピックアップして説明していきたいと思います。
 
0.そもそもメモリとは
参照型変数について理解する前に、まずはメモリについて意識する必要があります。
 
メモリとは、プログラムの実行中に取り扱っているデータを一時的に保存する領域です。
数学の問題を解く時に、答えを出す前に途中式を紙に書くと思いますが、その紙がメモリのようなものだと考えれば良いです。
なお、一般的には、プログラムの実行結果は最終的にファイルやDBに恒久的に保存するか、画面に表示するかします。
数学の問題の例で言うと、恒久的な保存領域や画面表示が答えに相当すると考えれば良いです。
 
そして、メモリは1バイト(8ビット)ずつ細かく区分けされており、それぞれの区分けについて「アドレス」と呼ばれる値により一意に場所を特定します。
アドレスのバイト数は、64ビットのOSの場合は8バイトになります。
 
文章で書かれてもイメージが難しいと思いますが、後ほど図を使って説明します。
とりあえず、「メモリと呼ばれる一時的な保存領域がある」という意識を持っていただければ、と思います。
 
1.プリミティブ型と参照型
Javaの変数は、プリミティブ型と参照型の2種類に大きく分けることができます。
どちらの型なのかによって、メモリに格納する内容が変化します。
 
プリミティブ型に分類される型については、以下の8つの型が存在します。

細かいですが、boolean型についてはデータの保持に使うのは1ビットのみで、メモリに値を保存する時にはバイト単位になっているはずです。
Javaの実装上は1バイトになることが多いようなのですが、boolean型が何バイトになるのかは正式な取り決めはないので、万が一これを気にする必要がある場合は調査した方が良いです。
 
そして、プリミティブ型以外の型の変数は、全て参照型変数に分類されます。
newでオブジェクトを生成する変数は全て参照型変数です。
また、プリミティブ型の配列についても、参照型変数に該当します。
プリミティブ型のラッパークラス(Integer型、Character型、等)やString型についても参照型変数に該当しますが、特定の場面においてはプリミティブ型に見えるような動きをします(詳しくは後述します)。
 
プリミティブ型変数と参照型変数では、メモリに格納する値が異なります。
プリミティブ型変数では値そのものをメモリに格納するのに対し、参照型変数ではメモリ上にその変数用の保存領域を確保した上で、その保存領域の場所を指し示すアドレスを変数の領域に格納します。
図に表すと、以下のようになります。

この違いは、プログラムの挙動の違いとなって現れます。それを以降で説明していきます。
 
2.変数をコピーした際の挙動の違い
プリミティブ型変数と参照型変数の挙動の違いを実感するのは、変数をコピーした時でしょう。
実務でも躓きやすいポイントなので、サンプルコード付きで詳しく説明していきます。
 
以下は、プリミティブ型の変数をコピーした後に、コピー元の変数を変更する例です。
コピー元の変数の変更は、コピー先の変数に影響しません。
これはイメージ通りだと思います。
 
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        int i1 = 1;
        
        // 変数のコピー
        int i2 = i1;
        
        // コピーした変数の表示
        System.out.println(i2);
        
        // コピー元の変数の変更
        i1 = 2;
        
        // コピーした変数の再表示
        System.out.println(i2);
    }
}
 
1
1
 
しかし、同じような書き方で参照型変数をコピーした場合、コピー元の変数の変更がコピー先の変数にも影響してしまいます。
以下は、配列をコピーした例と、ArrayList型をコピーした例です。
 
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        int il1 = {1,2,3};
        
        // 変数のコピー(シャローコピー)
        int
il2 = il1;
        
        // コピーした変数の表示
        System.out.println(il2[1]);
        
        // コピー元の変数の変更
        il1[1] = 4;
        
        // コピーした変数の再表示
        System.out.println(il2[1]);
    }
}
 
2
4
 
public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        ArrayList<Integer> il1 = new ArrayList<>();
        il1.add(1);
        il1.add(2);
        il1.add(3);
        
        // 変数のコピー(シャローコピー)
        ArrayList<Integer> il2 = il1;
        
        // コピーした変数の表示
        System.out.println(il2.get(1));
        
        // コピー元の変数の変更
        il1.set(1,4);
        
        // コピーした変数の再表示
        System.out.println(il2.get(1));
    }
}
 
2
4
 
なぜこのようなことが起こるのかと言うと、この書き方ではアドレス値をコピーしてしまっており、領域を新たに確保しているわけではないからです。
このようなコピーは、シャローコピー(浅いコピー)と呼ばれます。
 
シャローコピーを図として表すと以下のようになります。

 
コピーした段階では、アドレス値をコピーしてしまっており、指し示す領域はコピー元と同一になってしまっています。

そのため、コピー後にコピー元の変数を変更すると、その影響がコピー先の変数にも表れてしまいます。

これを回避するためには、新たに領域を確保し、その領域に格納する値をコピーした後、新たな領域を指し示すアドレス値を変数に格納するような形でコピーする必要があります。
このようなコピーは、ディープコピー(深いコピー)と呼ばれます。
 
ディープコピーを図解すると以下のようなイメージになります。

コードで言うと、以下のようなコードになります。
 
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        int il1 = {1,2,3};
        
        // 変数のコピー(ディープコピー)
        int
il2 = {0,0,0};
        for (int i = 0; i < il1.length; i++) {
            il2[i] = il1[i];
        }
        
        // コピーした変数の表示
        System.out.println(il2[1]);
        
        // コピー元の変数の変更
        il1[1] = 4;
        
        // コピーした変数の再表示
        System.out.println(il2[1]);
    }
}
 
2
2
 
public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        ArrayList<Integer> il1 = new ArrayList<>();
        il1.add(1);
        il1.add(2);
        il1.add(3);
        
        // 変数のコピー(ディープコピー)
        ArrayList<Integer> il2 = new ArrayList<>(il1);
        
        // コピーした変数の表示
        System.out.println(il2.get(1));
        
        // コピー元の変数の変更
        il1.set(1,4);
        
        // コピーした変数の再表示
        System.out.println(il2.get(1));
    }
}
 
2
2
 
上記のコードは原始的なディープコピーの方法です。
ディープコピーを行うためのCloneableインターフェースがJavaでは用意されており、これを使用した方が効率的にディープコピーを行えることもあります。
詳しくは「java:オブジェクトの中身をコピーする方法(cloneメソッド実装)(https://cyzennt.co.jp/blog/2020/01/24/java%ef%bc%9a%e3%82%aa%e3%83%96%e3%82%b8%e3%82%a7%e3%82%af%e3%83%88%e3%81%ae%e4%b8%ad%e8%ba%ab%e3%82%92%e3%82%b3%e3%83%94%e3%83%bc%e3%81%99%e3%82%8b%e6%96%b9%e6%b3%95%ef%bc%88clone%e3%83%a1%e3%82%bd/)」を参照して下さい。
 
なお、プリミティブ型変数と参照型変数の挙動の違いを実感する代表的な場面としては、他にはメソッドの引数に変数を引き渡す場面が挙げられます。
情報処理技術者試験では値渡しと参照渡しの違いについても出題範囲になっていますが、これは値そのものを渡しているか、アドレス値を渡しているかの違いです。
参照型変数を引数で渡す時にアドレス値を渡している感覚(参照渡しをしている感覚)を持っていないと、これもバグの原因になり得ますので、注意が必要です。
 
3.メモリの開放
参照型変数の値を保持するための領域は、newする度に確保され直します。
(配列の場合は、配列を宣言し直す度に確保され直されます)
 
例えば、以下のコードでは、2回目のnewにより領域が再確保されています。
 
import java.util.*;
import java.util.ArrayList;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        ArrayList<Integer> il1 = new ArrayList<>();
        il1.add(1);
        il1.add(2);
        il1.add(3);
        
        // 変数の表示
        System.out.println(il1.get(1));
        
        // メモリ領域の再割り当て
        il1 = new ArrayList<>();
        il1.add(4);
        il1.add(5);
        il1.add(6);
        
        // 変数の再表示
        System.out.println(il1.get(1));
    }
}
 
2
5
 
これを図解すると以下のようになります。

ここで問題になるのが、newし直す前に確保していた領域です。
この領域はプログラムで確保していたものの、使われなくなった領域です。
newで確保した領域が他のプロセス(プログラム)から使われることはあってはならないので、確保した領域は他のプロセスから使えなくなるような制御がかかります。
再び他のプロセスから使えるようにするためにはメモリを開放する必要があるのですが、この開放を忘れたまま領域の再確保を繰り返すと、他のプロセスが使えるメモリの領域が徐々に減っていきます。
これが「メモリリーク」と呼ばれる現象であり、放置すると空きメモリの不足により、プロセスの挙動や、場合によってはシステム全体の挙動が不安定になります。
 
C言語では、メモリの開放はコーディングにより明示的に行う必要がありました。
しかし、Javaでは「ガベージコレクション」と呼ばれる仕組みにより、開放するべき領域がある程度増えたら自動的に開放が行われるようになりました。
 
意図しないタイミングでのガベージコレクションは予期せぬ性能劣化を招く可能性があるので、性能要件がシビアなシステムではガベージコレクションについても気を配る必要があります。
ガベージコレクションについて詳しく見ていくにはこの記事ではとても足りないのですが、調べる上でメモリのイメージがついていれば理解は早まると思います。
 
4.イミュータブルな参照型変数について
参照型変数の中には、イミュータブルな変数も存在します。
イミュータブル(immutable)とは「不変」という意味であり、ミュータブル(mutable)の対義語です。
オブジェクト指向言語においては、「イミュータブル」は、「オブジェクトの生成後に、そのオブジェクトの状態(メモリ領域に保持されている値)が変化しない」という意味を指します。
 
イミュータブルな参照型変数の場合、newして領域を確保した後にオブジェクトの状態を変更したい場合は、再度newして領域を確保し直す必要があります。
 
Javaにおいては、プリミティブ型のラッパークラスやString型が、イミュータブルな参照型として用意されています。
これらの変数については、newを書かなくとも、領域が都度確保され直す、という挙動となります。
その結果、値の代入や参照においては、あたかもプリミティブ型の変数かのような挙動となります。
(ただし、本当にプリミティブ型の変数というわけではないため、プリミティブ型変数と異なりnull値を持つことができ、メソッドも持っています)
 
Javaで用意されているイミュータブルな参照型変数のコピーする例は以下の通りとなります。
これはシャローコピーと同じ書き方ですが
 
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        Integer i1 = 1;
        
        // 変数のコピー
        Integer i2 = i1;
        
        // コピーした変数の表示
        System.out.println(i2);
        
        // コピー元の変数の変更
        i1 = 2;
        
        // コピーした変数の再表示
        System.out.println(i2);
    }
}
 
実際の挙動としては以下のように明示的にnewしているのと同じ挙動となるため
 
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // 変数の宣言
        Integer i1 = new Integer(1);
        
        // 変数のコピー
        Integer i2 = new Integer(i1);
        
        // コピーした変数の表示
        System.out.println(i2);
        
        // コピー元の変数の変更
        i1 = 2;
        
        // コピーした変数の再表示
        System.out.println(i2);
    }
}
 
結果としてはディープコピーの挙動となります。
(つまり、シャローコピーすることはできず、意図せずともプリミティブ型のような動きとなります)
 
1
1

効率的な会議の進め方

仕事を進める上で会議は欠かせません。
 
例えば、以下の目的で会議を行います。
・管理表やチケットで管理されたタスクの棚卸
・要件や作業状況等のヒアリング
・問題に対する対応案の検討
・成果物に対するレビュー
 
会議を上手く運営すると仕事はスムーズに進みますが、会議には複数の人の時間を取らせてしまうという問題もあるため、効率的な運営が欠かせません。
ここでは、会議を効率的に進めるために何を心がければ良いのかを書いていきます。
 
1.会議を開催するべきか考える
 会議を行う利点として、
 リアルタイムに相互にコミュニケーションが取れるというものがあります。
 逆に言うと、その利点を活かせないのであれば、
 会議を行わずに、テキストベースでやりとりして各自が作業した方が効率が良いです。
 
 リアルタイムでの相互のコミュニケーションが必要になる場面としては、
 他の人の反応により結論が変わる可能性や頻度が高い場面になります。
 例えば、要件のヒアリングは話を聞くまでどのような話(結論)になるか
 予測するのか難しいので、会議を開催する必要性が高いです。
 逆に、プログラムの不具合の調査は、
 不明点が見つからない限りは自分一人で結論を出すことができるので、
 会議を開催する必要性が薄いです。
 
 このように、まずは、会議を行う必要があるかどうかを考えるべきです。
 
 なお、この時点で誰とリアルタイムにコミュニケーションすれば良いかが見えるので、
 会議に召集する必要がある参加者も見えてきます。
 
2.下準備を行う
 会議を行うことに決めたら、次に下準備を行います。
 会議中に自分一人で行うべきタスクを実施してしまうと時間がもったいないので、
 そのような作業は予め実施しておく必要があります。
 
 例えば、タスクの棚卸を行うのであれば、
 自分が把握しているタスクは全て管理表やチケットとして起票するべきです。
 また、成果物のレビューを行うのであれば、
 レビュー対象の成果物を叩き台の形で良いので一旦作成する必要があります。
 
 他の方に準備を依頼するのであれば、先にその旨を通知すると良いでしょう。
 準備の依頼が必要ないように思える場合についても、
 予め準備したいと考える参加者がいるかもしれないので、
 その場合お会議の議題やアウトラインは先に通知した方が望ましいです。
 
 なお、会議開催までに事前準備を完了できる見込みであれば、
 先に3を実施して時間を押さえてしまった方が良い場合もあります。
 
3.時間を設定する
 会議の準備の一貫として、参加者の時間を押さえる必要があります。
 
 特に、組織の上位者は会議に呼ばれることが多く、
 空いていない時間が多いので、上位者から先に押さえるのがポイントになります。
 
 会議の時間を押さえるのが難しいのであれば、
 時間設定は先に行ってしまった方が良いです。
 会議への参加が必須ではない方については、
 任意参加という形で会議が存在することだけ通知し、
 参加するかどうかを判断してもらい、
 参加しないにしても後で結果を展開する、
 という形にすると良いでしょう。
 
4.会議を実施する
 ここまで準備ができていれば、会議の目的も内容も明確なので、
 準備時に考えていた通りに会議を進めるだけです。
 
 ここで重要になるのは、会議の中で結論を決めることです。
 より具体的に言うと、
 誰が何をやるのか、という次のアクションを決める必要があります。
 
 例えば、要件のヒアリングであれば、
 ヒアリング結果を元に誰がいつまでに次の提案を行うのか、
 ヒアリングしきったのであれば誰がいつまでに要件定義書に書き展開するのか、
 ということを決める必要があります。
 また、成果物のレビューであれば、
 誰がいつまでにレビューの指摘事項に対応するのか、を決める必要があります。
 
 会議結果を忘れないように、
 議事録のようなドキュメントに残し展開するのも重要です。

相手の拘りに合わせて柔軟に計画を変更する

計画を策定する場合、自分に最終的な決定権がないという状況が往々にして起こり得ます。
具体的には、取引先や上司、先輩、プロパーの決定に従わざるを得ない、ということは少なくありません。
 
このような場合、自分が推す案に拘り、無理に押し通そうとするのは賢い判断ではありません。
組織の中で仕事をするにあたっては、自分の方が折れて相手に従う方が賢いです。
 
しかし、自分が異なる案を推しているということは、相手が決定した案には何かしらの問題があることに気付いているはずです。
相手の決定を覆さない形で、問題を解消する別の方法を提案するのが賢い判断になります。
 
----
 
例えば、データベースの設計方針で、外部キーを使うかどうかを検討しているとします。
外部キーを使用した場合、データ不整合(親テーブルに存在しない項目が子テーブルに存在する)をデータベースの機能により防ぐことができます。
しかし、テストデータを作成する時やデータを修正する時に、子テーブルを先に挿入・更新できなくなるケースがあり、利便性が下がります。
 
ここで、自分が外部キーを使用するべきと考えているのに対して、決定権がある相手は外部キーを使用するべきではないと考えているとします。
この場合、相手に合わせる形で外部キーを使用しない方向で考えつつ、外部キーを使わなかった場合の問題を解消する方法を別に考えるのが賢い判断です。
 
子テーブル・親テーブルを挿入・更新・削除した際に、最終的にデータ不整合を防ぐことができれば良いので、設計・製造・テストや運用の担当者向けに、どのテーブルとどのテーブルが親子関係にあるのか、親子関係を示すキー項目はどれか、というのをドキュメントに残し、整備・展開し、各自で気を付けてもらう、という方法が考えられます。
また、各自で気を付けても、ミスによりどうしても不整合は発生し得るので、定期的に親子関係が失われたデータを発見するプログラム・スクリプトを走らせるのも良い方法かもしれません。

ディベートから学ぶ案の検討の方法

仕事をする上では、システムの計画や設計等で案を検討することがあります。
検討する際に、ディベートの考え方を用いると、肯定・否定に偏ることなく案を検討しやすくなります。
 
この記事では、案を検討する上で参考になるディベートの考え方を簡単に説明したいと思います。
 
----
 
1.案の具体化
案を検討する上では、案を具体化する必要があります。
(具体化しないまま議論を進めても、案のメリットもデメリットも不明確になります)
 
例えば
「あるJavaの保守開発の現場で、コーディング規約を新たに導入するべきである」
というテーマがあるとします。
 
このテーマを具体的な案にすると、以下のようになります。
・「オブジェクト倶楽部Javaコーディング標準」をコーディング規約として導入する
・コーディング規約をドキュメントとして開発者へ広める
・レビューの際にはコーディング規約通りかどうかを確認する
 
2.案のメリット・デメリットの組み立て
案には、導入することによるメリットとデメリットがあります。
具体的な案が決まったら、メリットとデメリットを組み立てる必要があります。
 
メリットとデメリットは、以下の要素が含まれる主張を掛け合わせることで、
組み立てることができます。
 
■メリット
・内因性 …案を導入していない現状で問題が発生しているという主張
・解決性 …案を導入することで問題が解決するという主張
・重要性 …問題を解決することは重要であるという主張
 
■デメリット
・固有性 …案を導入していない現状では問題がないという主張
・発生過程…案を導入することで新たに問題が出るという主張
・深刻性 …新たに発生する問題が重大であるという主張
 
コーディング規約の例で考えると、以下のような主張が考えられます。
 
■メリット
・内因性 …ソースファイル毎でソースの書き方がバラバラである
・解決性 …コーディング規約により、今後改修されるソースは書き方が統一される
・重要性 …ソースの書き方が統一されれば可読性が高まり不具合が減る
 
■デメリット
・固有性 …現状では1つ1つのソースファイル内では書き方が統一されている
・発生過程…改修時に規約に従うと、ソースファイル内で書き方が不統一になり得る
・深刻性 …ソースファイル内で書き方が不統一になることで逆に可読性が低下する
 
3.主張を支えるための根拠
主張を組み立てることでメリット・デメリットを構成できますが、
その主張自体が正しいかどうかを示す必要はあります。
それを示すものが、根拠です。
 
根拠としては、書籍や記事、調査結果といったものが考えられます。
 
例えば
「現状では1つ1つのソースファイル内では書き方が統一されている」
と主張したい場合は、
色々な機能からソースファイルをピックアップし、調査し、例示すると良いでしょう。
 
4.メリットとデメリットを比較するための価値基準
メリット・デメリットを組み立て、主張が疑わしくない(根拠がしっかりしている)、
ということが確認できたら、次はメリットとデメリットを比較する必要があります。
メリットの方が大きければその案は採用するべき、
デメリットの方が大きければその案は採用するべきではない、
となります。
 
メリットとデメリットの大きさの比較は主観に頼らざるを得ない場合が多く、
判断が難しいです。
そこで重要になるのが、「価値基準」です。
議論の対象となっている組織で何を大切にしているのかを明確にすることで、
メリットとデメリットの比較が容易になります。
 
例えば、前述のコーディング規約の例の場合、
長期的に見た場合のコードの可読性を重視するのか、
近視眼的に見た場合のコードの可読性を重視するのかで、
判断が変わってきます。
自組織が前者の場合はメリット、後者の場合はデメリットの方を重視するべきでしょう。
 
5.返しの案と代替案
デメリットが発生しないようにするための返しの案を併用することで、
デメリットが発生しなくなり、案を採用する方向に持って行ける場合があります。
逆に、メリットのみが発生するような代替案を用意することで、
代替案が一番良いという結論に持って行ける場合があります。
より議論を深めるのであれば、返しの案や代替案も考えた方が望ましいです。
 
例えば、前述のコーディング規約の場合
・ソースファイル内の書き方の統一性が失われる場合は規約を適用しなくて良いとする
という返しの案を併用することで、デメリットの発生を防ぐ効果を期待できます。
 
また、代替案として
・色々なソースの書き方について教育し、色々な書き方に慣れてもらう
という案を採用することで、
デメリットがある元々の案を採用しなくても良いという結論になるかもしれません。
 
----
 
このように考えて案を検討するのは面倒かもしれません。
実際の仕事では、論理的に案を検討するよりも、スピード感を持って判断する方が重要かもしれません。
 
しかし、誤った判断をしないために、論理的に考える場合はこうなる、というのは知っておいて損はないと思います。
特に、メリットとデメリットの両方を検討する、という視点は重要になるでしょう。

ゲーム理論を現実世界へ適用するにあたっての問題点

この記事は「ゲーム理論:物事をゲームとして正しく認識するための方針」を焼き直したものです。
 
----
 
ゲーム理論は話だけ聞くと簡単そうに見えますが、実際に適用しようとするとある壁にぶつかります。
その壁とは、「適用対象をゲームとして正しく認識することが困難」という壁です。
ゲームとしての認識が誤っていると、ゲーム木や利得表を正しく書くこともできなくなり、そこから導き出される分析結果も誤ったものになってしまいます。
 
今回の記事では、この問題について例を挙げて説明し、どのような落とし穴があるのかを詳しく書いていきます。
 
ゲーム理論のおさらい】
ゲーム理論については、下記の記事に簡単に書いています。
情報処理技術者試験対策「ゲーム理論」 (https://akira2kun.hatenablog.com/entry/2018/07/10/234859)
 
ゲーム理論の3つの前提条件】
ゲーム理論で分析を行うためには、以下の3つの前提条件が必要になります。
「適用対象をゲームとして正しく認識する」を具体的に言うと、「以下の3つの前提条件を正しく設定する」ということになります。
 
・利害関係のあるプレイヤーの洗い出し
ゲーム理論とは、自分が選んだ選択肢と相手が選んだ選択肢の組み合わせで結果がどのように分岐するのかを分析する理論である。
そのため、まずは結果に影響を与えるような利害関係のあるプレイヤーを洗い出す必要がある。
 
・各プレイヤーが持つ選択肢
前述の通り、ゲーム理論とは選択肢を選んだ結果を分析する理論であるため、その「選択肢」を洗い出す必要もある。
 
・選択肢を選んだ結果得られる利得
ゲーム理論における「結果」は「利得」と呼ばれているが、その利得の大小も定義する必要もある。
 
【前提条件を設定する難しさ】
上記の前提条件は、ゲーム理論の例題では自明であり、スポーツ・ボードゲームコンピューターゲーム等でもルールとして自明に近い形で提示されています。
しかし、現実世界の問題では前提条件は自分で設定する必要があり、スポーツ・ボードゲームコンピューターゲーム等に対してゲーム理論で厳密に分析する場合にも前提条件を疑う必要があります。
 
これらの前提条件を設定するのは意外と難しいです。
以下では、ゲーム理論の代表的な例題である囚人のジレンマhttps://d.hatena.ne.jp/keyword/%E5%9B%9A%E4%BA%BA%E3%81%AE%E3%82%B8%E3%83%AC%E3%83%B3%E3%83%9E)での例を挙げて説明します。
 
・利害関係のあるプレイヤーの洗い出し
一見利害関係がありそうなプレイヤーは実は利害関係がなかったり、逆に意外なプレイヤーと利害関係があったりします。
囚人のジレンマの例では、相手の囚人の行動が自分の量刑に影響しないのであれば、相手の囚人をプレイヤーとして仮定するのは不適です。
また、被害者の気分で量刑が変わるのであれば、被害者をプレイヤーとして見立てるべきです。

 

・各プレイヤーが持つ選択肢
発想を膨らませると、選択肢も色々なものが想定できます。
囚人のジレンマの例では「黙秘」「自白」のみが選択肢として与えられていますが、囚人の能力や状況次第では「賄賂支払」「脱走」といった選択肢も想定する必要があります。

 

・選択肢を選んだ結果得られる利得
各プレイヤーの目的や価値観、心理的バイアス、外部から与えられた要素等によって、実際に感じる利得は変化します。
例えば、囚人のジレンマにおいては、プレイヤーには「刑を免れる」以外の目的はないことが前提として置かれています。
しかし、現実世界では「正義の主張」という目的が潜んでいる可能性があります。
この場合、自白をすることで「正義の主張」という目的を果たすことができ、それがプレイヤーの主観的な利得に影響を与え、そのプレイヤーにとっては自白一択になる可能性があります。


ゲーム理論を現実世界で用いる上での心構え】

ここまでで述べたように、ゲーム理論の前提条件の設定には難しさがあります。
 
ゲーム理論を現実世界で用いるためには、現在のゲームがどのようなゲームなのか、メタ的な目線で分析することが欠かせません。
囚人の量刑を決める場面で常に囚人のジレンマが発生するとは限りません。
先入観に捉われず、現在の状況をゲームに置き換えるとどうなるのか、というのをその場その場で考える必要があります。
 
その上で、ゲームを作り変える、という視点があるとより有効にゲーム理論を活用することができます。
例えば、囚人のジレンマの例で言うと、予め財を成しておけば、「賄賂支払」という選択肢が選択肢が生まれ、相手の選択肢に関わらず無実を勝ち取れるゲームを作り出すことができます。
どのようにゲームを作り変えれば良いのか、を考える上でも、ゲーム理論の現状分析は役に立つと思います。