1/14/17

自動テストが威力を発揮するビジネス環境

はじめに

今日は、自動テストが威力を発揮する、ビジネス環境について考察したいと思います。
今までの筆者の経験から、筆者が重要だと思う「自動テストが威力を発揮するビジネス環境因子」を3つあげます。
  1. コードベースに触るチームメンバーの人数が多い
  2. メンバーの入れ替わりが激しい
  3. コード変更を短期的に繰り返す
逆に言うと上記の3つに当てはまらない環境の場合、自動テストの導入の効果は薄いです。
つまり
    1. コードベースに触るチームメンバーの人数が少ない
    2. メンバーがほとんど入れ替われない
    3. 一度リリースしたら、コード変更はバグがあったときのみ (一回発注・納品型スタイル)
    の場合、自動テスト導入しても、効果は薄いと思われます。

    以下、重要だと思う因子3つの根拠を記します。


    3つの因子の根拠

    1. コードベースに触るチームメンバーの人数が多い
    当たり前ですが、関わるメンバーが多ければ多いほど。誰がどこを直したかをメンバー間で共有することは難しくなり、コードの変更管理コストも上昇していきます。Aという変更と別メンバーのBという変更が合わさったことによって、システムに矛盾した振る舞いを引き起こさせる可能性もあります。
    そういった状況でも、自動テストがあれば、少なくとも現状のロジックが破壊された場合には検知してくれるので、メンバーが多くても安心して開発を進めることができます。

    もちろん、対象のコードが自動テストでカバーされている + 正しいビジネスロジックのテストで網羅されているということが前提条件になります。

    2. メンバーの入れ替わりが激しい
    メンバーの入れ替わりが激しい場合、各メンバーが保有している知識が断片的で、システム全体の知識を持っている人が少ない、あるいは全くいないということが考えられます。またメンバーの入れ替わり画激しいため、知識は容易に失われてしまう可能性も高いです。
    自動テストがあれば、自分の変更が、思いがけないところでロジックを破壊していても検知できますので、安心です。

    ただし、その入れ替わっていくメンバーには、そのメンバーの責任でテストを追加してもらうことが必須になります。あまり移動しないコアメンバーがいるのであれば、そのコアメンバーがテストコードを書くほうが望ましいです。コアメンバーの方がシステム全体を把握して効率的かつ効果的なテストコードを書くことができるからです。コアメンバーがテストコードを書く負担が増えると考える方もいるかもしれませんが、入れ替わっていくメンバーに対して毎回システムの説明、コードレビューをするよりは、初回多少コストがかかっても自動テストを作成して、入れ替わっていくメンバーのコード変更のリスクを縛ってしまう方が圧倒的に効率がよいです。コードレビューはどうしても人間が行うため、精度にむらが出がちです。しかしコンピュータは、反復単純作業をミスなくこなすことに特化していますので、そういった意味でもコードの最低限の担保は自動テストに任せる方が適切です。

    3. コード変更を短期的に繰り返す
    あるシステムで作成した自動テストが、他のシステムで流用できることはまずありません。ですので、毎回異なるものを開発・納品を繰り返すような一回納品型のビジネスモデルですと、自動テストを追加するだけコストがかさむだけになってしまい、自動テストを導入するメリットは下がってしまいます。

    というわけで自動テストは、同じコードベースに対して追加的にすばやく変更を加えていく場合に、特に有効になります。


    おわりに

    自動テストが威力を発揮するビジネス環境について、筆者が重要だと思う因子を3つあげました。
    1. コードベースに触るチームメンバーの人数が多い
    2. メンバーの入れ替わりが激しい
    3. コード変更を短期的に繰り返す
    上記の条件に当てはまる環境でソフトウェア開発を進めている方は、自動テストの導入を検討して頂いてみてはいかがでしょうか。

    1/13/17

    BDD (Behavior-driven Development) について

    最近BDD (Behavior-driven Development) についてエンジニア仲間で話題になったので、記事を書こうと思い立ちました。

    結論から言うと、筆者は、BDDには懐疑的な立場です。

    BDDとは(あくまで筆者の理解)

    BDD (Behavior-driven Development)は、
    ビジネス側が書いたシナリオ(誰が、どんなとき、どの条件で、どうしする)や仕様をもとに、コンピュータで実行可能なテストを自動生成し、システム側がそのテストを通るようにシステム開発をしていく方法です。ビジネス側がBehaviorを書く際の言語は、ビジネス側でもできるだけ理解できる自然言語風の、BDD専用言語です。

    BDDのメリットは、

    1. ビジネス側の仕様記述から、システムテストを作成できるので、ビジネス側の望んでいるシステムを素早く正確につくれる
    2. ビジネス側の仕様変更がダイレクトにシステムテストに反映されるので、仕様変更にも強い

    ことかと思います(筆者の理解)。

    筆者がBDDを支持しない理由

    1番目の理由は、そもそもビジネス側の期待している振る舞い (Behavior) 自体が曖昧で、それを論理的に整合性のある仕様に落とし込むことが、システム開発で最も困難な場合が多いと感じるからです。

    ビジネス側の期待している振る舞いが簡潔で、論理的であればあるほとプログラムとして記述しやすくなるはずなので、その場合は、システム側だけのTDD (Test Driven Development) 方式で開発を進められると思います。

    BDDの記述方法も所詮は論理記述の一種なので、それが綺麗に書ける人はそもそも、エンジニアとしての才能があるので、普通に綺麗な仕様を書ける場合が多いのではないでしょうか。

    2番目の理由はBDDの記述言語です。
    自然言語風にするのはいいのですが、自然言語に近づければ近づけるほど、論理の矛盾や文法チェックが必要になり意味解釈が面倒になると思います(自然言語処理が簡単であれば、自然言語処理という研究分野は存在しないはず)。

    もちろんBDDの記述言語はプログラム言語を自然言語風にしたシナリオ記述言語なので、意味解釈の面で問題が出ることはないと思います。
    しかしそうすると、BDDの記述言語には表現に制約があり、最終的な表現の制約はプログラミング言語と同等の制約となるはずです。ビジネス側の担当者は、制約があるゆえに、無理にBehavior記述しようとしてBDDの記述言語に対して煩わしさを感じるかも知れません。
    結局、BDDの記述言語以外にも資料が必要になり、BDD導入前とシステムの開発スピードや維持コストは下がらなくなってしまうことになりかねません。

    逆にシステム側でプログラミング言語に熟達している人であれば、BDDの記述言語を覚えるのは面倒以外の何物でもありません。

    筆者には、BDDを記述は、「なんちゃらツクール」といったプログラムを書かずにゲームを作れるツールやゲームの「シナリオエディタ」を使う感覚が一番近いのかなと感じました。

    筆者は、プログラミング言語が、あの限られた表現制約の中で、多様なシステムを生み出す能力に感心しています。また逆に、あれだけ制約のあるプログラミング言語ですらも、いわゆる「クソコード」が生み出してしまうことに、いら立ちを感じることもあります。


    終わりに

    人間は非論理的な生き物であり、コンピュータは完全に論理的なものだと、(少なくとも筆者は)思っています。
    現状のシステム開発では、「ビジネス側の矛盾も混じった要望」と「コンピュータの求める論理的完全性」の整合性をとるのが、最も難しい部分であり、その整合性をとることのできるシステムを構築できる人が、「優秀な技術者 (エンジニア/設計者/プログラマ)」なのではないかと思います。

    恐らく、AIが発達して、ビジネス側の書いた仕様書、スライドを掘り込むと、システムが完成するレベルになるぐらいでないと、BDDはメリットがないと思います。(でもそこまで言ったら、既にBDDではないかもしれませんね。)

    1/2/15

    Reduce Code & Focus on Return On Code

    Value of reducing code

    Reducing amount of code is one of the MOST REQUIRED & IMPORTANT skill in real software development workplace, especially in team based software development.
    Most of the case, reducing code is much more valuable than writing code!
    Why doesn't any computer science teach this MOST important skill!
    Let me point out benefits of reducing code:

    • The less code makes programmer be the easy to understand project!
      • The amount of code they should read is lesser!
      • Fledgling developer easy to understand!
      • You don't have to write much test code!
      • Complexity should tend to be lower!
    • Code duplication tends to be less!
    • Prevent regression!
    • You can only care about code which really lively works!

    Put it all together, These benefits finally leads "Improve code maintainability" and "Reduce maintenance cost".
    I mention just in case - reducing code doesn't mean you should merge a few "for loop" lines into single line.
    My principles for reducing code are:

    • Delete unused code if you think it won't be used or just leave it for reference. The code can be easily pullout if you use version control system.
    • Remove duplication and extract abstraction. Too much abstraction is quite bad, so this principle cannot apply in an automatic manner.
    • Remove comments unless you have time to maintain them properly. Keep proper comment costs much more than you think and obsolete or meaningless comment leads downcast of project.

    Most software developer (maybe including me) show off in a patronizing way about how much they can write code.
    And measure value of developer by the amount of code he or she can write.
    I think this measurement is completely wrong for team based software development.
    Less code makes much more quicker business action possible & reduces maintenance cost.

    Return on Code (ROC)

    Let me propose new measurement for business efficiency or productivity of software - Return On Code (ROC).
    ROC means if lines of code is less and less, the measurement is higher and higher! i.e. your code helps business (=makes money) much more efficiently.
    ROC is inspired by Return On Investment (ROI) which represents the benefit to the investor resulting from an investment of some resource.
    I think writing code is investment and its artifact (software) makes money. So it's natural to calculate this kind of measurement similar to ROI.
    Let's write code with a focus on business rather than just writing code unconsciously!!

    12/30/14

    Java: Identify Country From IP Address

    Identify Country From IP Address

    Many people sometimes would like to identify country from IP address when you check access log or something.
    Most of the people google with keywords like "ip address country" or "whois ip" or something, and then use the internet service which they find.

    In this post, I will show you program for identifying country from IP address.
    I wrote the program in Java, but if you an average developer you can easily translate into the program language you prefer.


    Using File Provided by RIR

    IP address is allocated, registered and managed by regional internet registry (RIR).
    There are five organization based on covering region:

    • African Network Information Centre (AfriNIC): Africa
    • American Registry for Internet Numbers (ARIN): the United States, Canada, several parts of the Caribbean region, and Antarctica.
    • Asia-Pacific Network Information Centre (APNIC): Asia, Australia, New Zealand, and neighboring countries
    • Latin America and Caribbean Network Information Centre (LACNIC): Latin America and parts of the Caribbean region
    • Réseaux IP Européens Network Coordination Centre (RIPE NCC): Europe, Russia, the Middle East, and Central Asia
    The each organization provides the RIR formatted file which tells "Which IP addresses are allocated to which country"
    So we use this file, then we can identify country from ip address! bravo!

    The RIR file format is quite simple, something like below:

    arin|US|ipv4|130.62.0.0|65536|19880609|assigned|f8c82702dc77343cbc6d17c0cb9f76d1
    

    The important parts are:

    • 2nd: country
    • 3rd: type
    • 4th: start ip address
    • 5th: number of ip addresses allocated from the start which is on the 4th column
    • 7th: status
    I think you should download the file yourself from the following locations and read the official document here.
    Then you can understand much more details rather than my too simplified explanation :P
    • http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest
    • http://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest
    • http://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest
    • http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest
    • http://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest


    Java Code

    The program can do below:

    • Download RIR files
    • Create IP address range cache file
    • Check country based on the IP address range cache file
    What you should improve:
    • I think you might be better to store the RIR file data or cache file data in database for actual application usage.
    • you should merge some continuous IP ranges for more efficiency. For simplicity, I left it as it is.
    Okie. Now I will show you the whole java program for identifying country from ip address.
    The advantage of this program is only using Java Standard Development Kit. i.e. no external library required!

    Core Code

    package com.dukesoftware.utils.net.ipaddress;
    
    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStreamWriter;
    import java.io.UnsupportedEncodingException;
    import java.math.BigInteger;
    import java.net.URISyntaxException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.regex.Pattern;
    
    public class IpAddressCountryIdentifier {
    
        public static void main(String[] args) throws URISyntaxException,
                IOException {
            IpAddressCountryIdentifier checker = new IpAddressCountryIdentifier("c:/temp");
            // you should not call following methods every ip address check
            // updating these files once a day should be fine
            // checker.saveRIRFiles();
            // checker.createIpAddressFileFromRIRFile();
            checker.loadIpAddessToMemory();
            System.out.println(checker
                    .guessCountry("ip address which you would like to test here"));
        }
    
        private static final String[] URLS = new String[] {
                "http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest",
                "http://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest",
                "http://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest",
                "http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest",
                "http://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest" };
    
        private final static Map<String, String> FILES = new HashMap<String, String>() {
            {
                Pattern pattern = Pattern.compile("^.+/");
                for (String URL : URLS) {
                    String fileBody = pattern.matcher(URL).replaceAll("");
                    put(URL, fileBody + ".txt");
                }
            }
        };
    
        private final Cache cacheV4 = new Cache(
                ipAddressStr -> ipAddressStr.contains("."),
                IpAddressCountryIdentifier::dotIPv4_to_BigInteger);
        
        private final Cache cacheV6 = new Cache(
                ipAddressStr -> ipAddressStr.contains(":"),
                IpAddressCountryIdentifier::colonIpV6_to_BigInteger);
        
        private final String directory;
    
        public IpAddressCountryIdentifier(String directory) {
            this.directory = directory;
        }
    
        public void downloadRIRFilesFromNIC() throws IOException {
            for (Entry<String, String> entry : FILES.entrySet()) {
                Utils.saveToFile(entry.getKey(),
                        new File(directory, entry.getValue()).getAbsolutePath());
            }
        }
    
        public void createIpAddressCacheFileFromRIRFile() throws IOException {
            File ipv4File = new File(directory, "ipv4.txt");
            File ipv6File = new File(directory, "ipv6.txt");
    
            ipv4File.delete();
            ipv6File.delete();
    
            for (Entry<String, String> entry : FILES.entrySet()) {
                File file = new File(directory, entry.getValue());
                try (BufferedWriter bwV4 = newWriter(ipv4File);
                        BufferedWriter bwV6 = newWriter(ipv6File)) {
                    Utils.processLine(file, line -> {
                        if (line.startsWith("#"))
                            return;
    
                        String[] parts = line.split("\\|");
                        if (parts.length < 7)
                            return;
                        String status = parts[6];
    
                        if (!(status.equals("allocated") || status
                                .equals("assigned")))
                            return;
                        String country = parts[1];
                        String type = parts[2];
                        String start = parts[3];
                        long value = 0;
                        try {
                            value = Long.valueOf(parts[4]);
                        } catch (NumberFormatException e) {
                            return;
                        }
    
                        try {
                            if (type.equals("ipv4")) {
                                bwV4.write(cacheV4.toCacheString(country, start,
                                        value));
    
                            } else if (type.equals("ipv6")) {
                                bwV6.write(cacheV6.toCacheString(country, start,
                                        value));
                            }
                        } catch (IOException e) {
                            throw new RuntimeException("IOError while reading and writing ip address file", e);
                        }
                    });
                }
            }
        }
    
        public void loadIpAddessToMemory() throws IOException {
            cacheV4.clear();
            cacheV6.clear();
            File ipv4File = new File(directory, "ipv4.txt");
            File ipv6File = new File(directory, "ipv6.txt");
    
            Utils.processLine(ipv4File, cacheV4::put);
            Utils.processLine(ipv6File, cacheV6::put);
        }
    
        public String guessCountry(String ipAddress) {
            if (cacheV4.isAcceptableIPString(ipAddress)) {
                return cacheV4.guessCountry(ipAddress);
            }
            if (cacheV6.isAcceptableIPString(ipAddress)) {
                return cacheV6.guessCountry(ipAddress);
            }
            throw new IllegalStateException("Unacceptable ip address format:"
                    + ipAddress);
        }
    
        private static BufferedWriter newWriter(File file)
                throws UnsupportedEncodingException, FileNotFoundException {
            return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(
                    file, true), "utf-8"));
        }
        
        public static BigInteger dotIPv4_to_BigInteger(String dottedIP) {
            String[] addrArray = dottedIP.split("\\.");        
            BigInteger num = BigInteger.ZERO;  
            BigInteger block = BigInteger.valueOf(256);
            for (int i = 0; i < addrArray.length; i++) {            
                int power = 3-i;
                BigInteger value = BigInteger.valueOf(Integer.parseInt(addrArray[i]) % 256);
                value = value.multiply(block.pow(power));
                num = num.add(value);
            }        
            return num;
        }
        
        public static BigInteger colonIpV6_to_BigInteger(String colonedIP) {
            String[] addrArray = colonedIP.split(":", -1);        
            BigInteger num = BigInteger.ZERO;
            BigInteger block = BigInteger.valueOf(65536);
            for (int i = 0; i < addrArray.length; i++) {            
                if(!addrArray[i].equals(""))
                {
                    int power = 8-i;
                    BigInteger value = BigInteger.valueOf(Long.parseLong(addrArray[i], 16) % 65536L);
                    value = value.multiply(block.pow(power));
                    num = num.add(value);
                }
            }        
            return num;
        }
    
    }
    

    Class for represents IP address ranges with country

    package com.dukesoftware.utils.net.ipaddress;
    
    import java.math.BigInteger;
    
    final class IpRange {
        private final BigInteger start;
        private final BigInteger end;
        private final String country;
    
        public IpRange(BigInteger start, BigInteger end, String country) {
            this.start = start;
            this.end = end;
            this.country = country;
        }
    
        boolean inRange(BigInteger value) {
            return value.compareTo(start) >= 0 && value.compareTo(end) <= 0;
        }
    
        public String getCountry() {
            return country;
        }
    
    }
    

    Class for managing IP Address range cache files

    package com.dukesoftware.utils.net.ipaddress;
    
    import java.math.BigInteger;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.function.Function;
    import java.util.function.Predicate;
    
    class Cache {
        private final List<IpRange> ranges = new ArrayList<>();
        private final Predicate<String> ipStringAcceptor;
        private final Function<String, BigInteger> ipStrToBigInteger;
    
        public Cache(Predicate<String> ipStringAcceptor,
                Function<String, BigInteger> ipStrToBigInteger) {
            this.ipStringAcceptor = ipStringAcceptor;
            this.ipStrToBigInteger = ipStrToBigInteger;
        }
    
        void put(String line) {
            String[] parts = line.split("\\|");
            put(parts[0], new BigInteger(parts[1]), new BigInteger(parts[2]));
        }
    
        private void put(String country, BigInteger start, BigInteger end) {
            ranges.add(new IpRange(start, end, country));
        }
    
        String guessCountry(String ipAddress) {
            if (isAcceptableIPString(ipAddress)) {
                BigInteger value = ipStrToBigInteger.apply(ipAddress);
                return searchInRange(value, ranges);
            }
            throw new IllegalStateException("Unacceptable ip address format:"
                    + ipAddress);
        }
    
        private static String searchInRange(BigInteger value,
                List<IpRange> ranges) {
            for (IpRange range : ranges) {
                if (range.inRange(value)) {
                    return range.getCountry();
                }
            }
            return "";
        }
    
        public String toCacheString(String country, String start,
                long value) {
            BigInteger startValue = ipStrToBigInteger.apply(start);
            BigInteger endValue = startValue.add(BigInteger.valueOf(value - 1));
            return country + "|" + startValue + "|" + endValue + "\n";
        }
    
        boolean isAcceptableIPString(String ipAddress) {
            return this.ipStringAcceptor.test(ipAddress);
        }
    
        void clear() {
            ranges.clear();
        }
    
    }
    

    Class for some trivial utility methods

    package com.dukesoftware.utils.net.ipaddress;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.FileReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.util.function.Consumer;
    
    public class Utils {
        
        public static final int _1K_BYTES = 1024;
    
        public final static void saveToFile(String url, String path)
                throws IOException {
            HttpURLConnection urlCon = null;
            try {
                URL urlObj = new URL(url);
                urlCon = (HttpURLConnection) urlObj.openConnection();
                urlCon.setRequestMethod("GET");
                try (InputStream is = urlCon.getInputStream();
                        OutputStream os = new FileOutputStream(path)) {
                    byte[] buffer = new byte[_1K_BYTES];
                    for (int bytes = 0; (bytes = is.read(buffer)) != -1;) {
                        os.write(buffer, 0, bytes);
                    }
                    os.flush();
                }
            } finally {
                if (urlCon != null) {
                    urlCon.disconnect();
                }
            }
        }
        
        public final static void processLine(File file, Consumer<String> lp) throws IOException{
            try(FileReader in = new FileReader(file);
                BufferedReader br = new BufferedReader(in)){
                String line;
                while ((line = br.readLine()) != null) {
                    lp.accept(line);
                }
            }
        }
    }
    
    

    Java: Coloned IPv6 Address To BigInteger

    IPv6 Address to Long

    I have written code for converting coloned IPv6 IP address to BigInteger value in Java.
    I have already written similar code for IPv4 IP address (see this post). The function is useful when you compare IP addresses based on numeric magnitude relationship.


    Java Code

    public static BigInteger colonIpV6_to_BigInteger(String colonedIP) {
        String[] addrArray = colonedIP.split(":", -1);        
        BigInteger num = BigInteger.ZERO;
        BigInteger block = BigInteger.valueOf(65536);
        for (int i = 0; i < addrArray.length; i++) {            
            if(!addrArray[i].equals(""))
            {
                int power = 8-i;
                BigInteger value = BigInteger.valueOf(Long.parseLong(addrArray[i], 16) % 65536L);
                value = value.multiply(block.pow(power));
                num = num.add(value);
            }
        }        
        return num;
    }
    

    Here is an example.

    // following code output "22170076769632982771575277020213308075606016"
    System.out.println(colonIpV6_to_BigInteger("FE80:0000:0000:0000:0202:B3FF:FE1E:8329"));