もし新人プログラマが「プリンシプル・オブ・プログラミング」を読んだら

概要

ブンブン Hello world.
はじめまして! ecbeing新卒1年目の開発です。
本名が開発です。部署名や役職名ではありません。
某有名Youtuberと同じ苗字なんです(血縁関係は恐らくありませんが)。

現在ecbeingチームで日々製品の改善等に努めております。
プロダクト開発チームは非常にメンバーの交流が活発で、学ぶことが多いなぁと感じる今日この頃です。

さて、そんなプロダクト開発チームではありますが、新人3人が配属され少し落ち着いたころに上司からこんな指令を頂きました。
「君たちには新人ミッションに取り組んでいただきます」

プロダクトの取り組み(新人ミッション)

前述のとおり、新人3人にはミッションが課されました。
内容としては「プリンシプル・オブ・プログラミング」をそれぞれ読み、各々が気になったトピックについてプレゼンするというものです。
書籍内容の深い理解や情報収集、基礎力の強化、チーム内の交流、プレゼンスキル、資料作成etc が目的なのかなぁなんて新人ながらに推測し取り組みました。
以降では本書の簡単な紹介と、開発がピックアップしたトピックついて説明します。

f:id:ecb_tkaihatsu:20191002131818j:plain
チーム内プレゼンの様子

書籍の内容について

プリンシプル・オブ・プログラミング(上田勲 著)は、比較的経験の浅いプログラマが、何を意識してプログラミングや業務に取り組めばよいかのイロハが記載された一冊です。
本書では「~の原理」や「~の法則」といった、プログラミングをするうえで必要となる考え方のベースであったり、何に注目して設計すべきかなどが幅広く記載されています。
タイトルの「プリンシプル」にもある通り抽象的/概念的な内容になっているため、本書を読めばすぐに良いコードが書けるようになるわけではありません。

しかし、プログラミングをするうえで何を考えてコーディングをするとよいか、どんなことに注目して設計をすればよいかなどの「考え方」を鍛える、広げるうえで重要な1冊であり、とても勉強になりました。

f:id:ecb_tkaihatsu:20191002131643j:plain
プリンシプル・オブ・プログラミング(実物)

KISS

本書の紹介はこのくらいにして、本題である私がピックアップしたトピックについて説明します。
私は今回、「KISS」をピックアップして紹介しました。
こちらはエンジニアリング全般、特にコーディング/設計において、常にシンプルさを追求するべきであるといったものです。
決してリア充がキャッキャウフフする的な話ではありません。

KISSとは(シンプルにしておけ、愚か者よ)

KISSは Keep It Simple, Stupid. または Keep It Short and Simple. の略で、先ほども説明したとおり「とにかくシンプルさに徹しろ」といったものです。
一般的にプログラム(ソースコード)は、手を加えれば加えるほど複雑になり、無秩序へと向かっていくことが多いです。
KISSはその現象に対して、少しでも複雑さを解消するためにとにかくシンプルであることが重要と唱えています。

余談になりますが、ソースコードのシンプルさについては本書でも至る所で挙げられており、とても重要な考え方ということがわかります。
逆に言えば、至る所でシンプルさについて語られてしまうほど、我々プログラマはカオスを生み出してしまいがちと捉えることができますが…

詳細(そもそも「シンプル」とはどういうことなのか)

シンプル とは

そもそも「シンプル」とはどういう事なのでしょうか?
改めて言葉の定義を説明しようとすると少々悩みますね…

コトバンクでは次のように解説されています。

簡潔なさま。また、飾り気やむだなところがなく、簡素なさま。

つまり「単純性」「簡潔性」を如何に担保できるか「シンプル」であるかどうかを左右します。
ソースコードに置き換えて語れば、各モジュールや関数などがいかに単純であるか、どれだけ簡潔な処理になっているかがシンプルなソースコードかどうかを決定することになります。

やりがちなあるあるパターン

シンプルなソースコードについて、定義は分かりました。
では、なぜこれらを意識する必要があるのでしょうか?
それは、プログラマのやってしまいがちな「あるあるパターン」があるからです。

1. テクニカルなコードを書いてしまう

我々プログラマは日々勉強し、この世界のスピードに適応していかねばなりません。
その過程でテクニカルな技術、トリッキーな技術を学ぶことも珍しくないですね。
プログラマも人間ですから、覚えたての技をついつい使ってみたくなってしまうこともあるでしょう。
しかしそれはいたずらにプログラムを難解にさせてしまうことが多いのです。
重要なのはユーザーに価値を提供することであって、頭の良さを示すことではありません。

2. 将来への布石

開発に携わっている際に「先々こんな機能が必要になるだろう」といった思考が頭を過ることがあります。
思い立ったが吉日、ついつい実装したくなりますね。
しかし、今現在不要なのであれば実装すべきではありません。
往々にして将来のために書いたコードは不要になることがあるからです。
必要な時に、必要な分を、最低限のボリュームで実現し常にシンプルさを追求することが重要です。

3. 要件の自己判断

ユーザーに要件について判断を仰ぐより、とりあえず自分でコードを書いてしまう方が手っ取り早いという事があると思います。
このような場合にプログラマが要件を判断し余計なコードを加えてしまうと、保守のための手間と時間を増やす結果になってしまいます。
あくまで要件を決めるのはユーザーであって、プログラマではありません。
常に必要とされてるものを明確にし、それのみの実現に注力すべきなのです。

上記のあるあるパターンは、いずれも複雑さへの片道切符であるため避けなければなりません。
新人も玄人も関係なく、プログラマなら誰しも意識したいところですね。

ソースコードによる実例

シンプルさについて一通り語ったところで、実際のコードにして確認してみましょう。
言うは易く行うは難し。
このコード例を作成するにおいて先輩方に色々と助けていただきました。ありがとうございます。
今回は観点の違う3つの例を示したいと思います。

1. 冗長なコード

まずは手始めに冗長なコードを見てみましょう

例1

// 掛け算
using System;

namespace TestCode_1
{
    class multiplication
    {
        static int Main(string[] args)
        {
            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 1, i, 1 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 2, i, 2 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 3, i, 3 * i);
            }
            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 4, i, 4 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 5, i, 5 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 6, i, 6 * i);
            }
            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 7, i, 7 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 8, i, 8 * i);
            }

            for (int i = 1; i <= 9; i++)
            {
                Console.WriteLine("{0} * {1} = {2}", 9, i, 9 * i);
            }
            return 0;
        }
    }
}

上記コードは単純な九九を出力するためのプログラムです。
同じようなfor文が何度も登場し非常に煩雑としています。
このようなコードは以下のように書き換えるといいでしょう。

例1 リファクタリング後

using System;

namespace TestCode_1
{
    class multiplication
    {
        static int Main(string[] args)
        {
            int minBaseNumber = 1;
            int maxBaseNumber = 9;
            int minTimes = 1;
            int maxTimes = 9;

            for(int baseNumber = minBaseNumber; baseNumber <= maxBaseNumber; baseNumber++)
            {
                new multiplication().CalcMultiplication(baseNumber, minTimes, maxTimes);
            }

            return 0;
        }

        void CalcMultiplication(int baseNumber, int minTimes, int maxTimes)
        {
            for(int times = minTimes; times<=maxTimes; times++)
            {
                CalculationResultOutput(baseNumber, times, baseNumber*times);
            }
        }

        void CalculationResultOutput(int baseNumber, int times, int result)
        {
            Console.WriteLine("{0} * {1} = {2}", baseNumber, times, result);
        }

    }
}

共通した繰り返し処理を1つのメソッドにすることで簡潔なコードになりました。
また、表示も1つのメソッドとすることで、表示を変更したくなった際にもどこを変更すればよいか簡単に判断ができますね。

2. 行数が短いコード

有名なfizzbuzz問題を例に見てみましょう。
以下のコードでは1行でfizzbuzz問題を解いています。

例2

using System;

namespace FizzBuzz
{
    class FizzBuzzSolver
    {
        static int Main(string[] args)
        {
            // fizz-buzz
            for(int i = 0; i<= 15; i++) Console.WriteLine("{0}:{1}", i, i % 15 == 0 ? i!= 0 ?"fizzbuzz":"" : i % 3 == 0 ? "fizz" : i % 5 == 0 ? "buzz":"");
            return 0;
        }
    }
}

短くまとまっていますね。
例がfizzbuzz問題のためまだ比較的簡単なコードではありますが、これが実際のシステムなどで用いられていたらどうでしょうか?
行数が短いだけではシンプルとは言えません。「単純性」が損なわれているからです。
もう少し単純に書くと以下のようになるでしょう。

例2 リファクタリング後

using System;

namespace FizzBuzz
{
    class FizzBuzzSolver
    {
        static int Main(string[] args)
        {
            int baseNumber = 0;
            int maxNumber = 15;
            FizzBuzzSolver fizzbuzzSolve = new FizzBuzzSolver();

            fizzbuzzSolve.FizzBuzzSolverMethod(baseNumber, maxNumber);

            return 0;
        }

        void FizzBuzzSolverMethod(int baseNumber, int maxNumber)
        {
            string judgedResult = "";
            for (int num = baseNumber; num <= maxNumber; num++)
            {
                judgedResult = this.JudgementFizzBuzz(num);
                this.FizzBuzzResultOutput(num, judgedResult);
            }
        }

        string JudgementFizzBuzz(int judgedNumber)
        {
            if(judgedNumber == 0)
            {
                return "";
            }
            else
            {
                if (judgedNumber % 15 == 0)
                {
                    return "fizzbuzz";
                }
                else if (judgedNumber % 3 == 0)
                {
                    return "fizz";
                }
                else if (judgedNumber % 5 == 0)
                {
                    return "buzz";
                }
                else
                {
                    return "";
                }
            }
        }

        void FizzBuzzResultOutput(int number, string result)
        {
            Console.WriteLine("{0}:{1}", number, result);
        }
    }
}

先ほどのコードに比べてだいぶ長くなりましたね。
人によっては「この程度のif文を書くなんて初級プログラマのようでダサい」なんて感じるかもしれません。
ダサくていいんです。分かりやすいコードであることの方が遥かに重要なのです。

特に業務においては基本チームでプロジェクトに取り組みます。
チーム全員のコーディング能力はバラつきがあるため、どんな人でも読めるコードが理想とされます。
そのための「単純性」「簡潔性」であるので、愚直に素直なコーディングを心がけるのが良いでしょう。

3. 修正により複数の機能が実装されたメソッド

ここでは何かしらの理由で複数の機能が実装されたメソッドについて見ていきます。
一つのプログラムに対して長く携わっていると、必ず何かしらの改善、改良をすることがあります。
この際に、既に実装してあるソースコードに対して追加で実装することで実現することも珍しくありません。
しかしこの際には、細心の注意を払って実装しなくてはなりません。
以下のソースコードは、足し算プログラムを改修した結果複数の機能が実装されてしまった演算プログラムです。

例3

using System;

namespace Calculation
{
    class Calculation
    {
        static int Main(string[] args)
        {
            Calculation calc = new Calculation();

            calc.CalculateOperationMethod();

            return 0;
        }

        void CalculateOperationMethod()
        {
            string commandStr = "";
            double firstNumber = 0.0;
            double secondNumber = 0.0;
            double result = 0.0;

            Console.Write("please input first number : ");
            firstNumber = double.Parse(Console.ReadLine());

            Console.Write("please input second number : ");
            secondNumber = double.Parse(Console.ReadLine());

            Console.WriteLine("please input calculation comannd");
            Console.WriteLine("ex.) addiction, subtraction, multiplication, division");
            Console.Write("->");
            commandStr = Console.ReadLine();

            result = this.CalculateOperation(commandStr, firstNumber, secondNumber);

            Console.WriteLine("calculation result = {0}", result);
        }
        
        double CalculateOperation(string commandStr, double firstNumber, double secondNumber)
        {
            if (commandStr == "addiction")
            {
                return firstNumber + secondNumber;
            }
            else if (commandStr == "subtraction")
            {
                return firstNumber - secondNumber;
            }
            else if (commandStr == "multiplication")
            {
                return firstNumber * secondNumber;
            }
            else if (commandStr == "division")
            {
                if (firstNumber != 0.0)
                {
                    return firstNumber / secondNumber;
                }
                else
                {
                    return double.NaN;
                }
            }
            else
            {
                Console.WriteLine("calculation command is wrong");
                return double.NaN;
            }
        }
    }
}

上記のコードでは、1つのメソッドに大して複数の機能が実装されています。
CalculateOperationMethodメソッドでは

  • コンソールでのインプット/アウトプット
  • 計算用メソッドの呼び出し

CalculateOperationメソッドでは

  • 足し算
  • 引き算
  • 掛け算
  • 割り算

のすべての計算を行っています。
これは「単純性」や「簡潔性」の観点からして、あまりシンプルではありません。
シンプルであるためにはメソッドの機能は1つに絞りたいところです。
このコードは、以下のように改良することができるでしょう。

例3 リファクタリング後

using System;

namespace Calculation
{
    class Calculation
    {
        static int Main(string[] args)
        {
            Calculation calc = new Calculation();

            calc.CalculateOperationMethod();

            return 0;
        }

        void CalculateOperationMethod()
        {
            string commandStr = "";
            double firstNumber = 0.0;
            double secondNumber = 0.0;
            double result = 0.0;

            this.CalculatingInformationInput(ref commandStr, ref firstNumber, ref secondNumber);
            result = this.CalculateOperation(commandStr, firstNumber, secondNumber);

            Console.WriteLine("calculation result = {0}", result);
        }

        void CalculatingInformationInput(ref string commandStr, ref double firstNumber, ref double secondNumber)
        {

            Console.Write("please input first number : ");
            firstNumber = double.Parse(Console.ReadLine());

            Console.Write("please input second number : ");
            secondNumber = double.Parse(Console.ReadLine());

            Console.WriteLine("please input calculation comannd");
            Console.WriteLine("ex.) addiction, subtraction, multiplication, division");
            Console.Write("->");
            commandStr = Console.ReadLine();
        }

        double CalculateOperation(string commandStr, double firstNumber, double secondNumber)
        {
            if (commandStr == "addiction")
            {
                return this.CalculateAddiction(firstNumber, secondNumber);
            }
            else if (commandStr == "subtraction")
            {
                return this.CalculateSubstraction(firstNumber, secondNumber);
            }
            else if (commandStr == "multiplication")
            {
                return this.CalculateMultiplication(firstNumber, secondNumber);
            }
            else if (commandStr == "division")
            {
                return this.CalculateDivision(firstNumber, secondNumber);
            }else
            {
                Console.WriteLine("calculation command is wrong");
                return double.NaN;
            }
        }

        double CalculateAddiction(double firstNumber, double secondNumber)
        {
            return firstNumber + secondNumber;
        }

        double CalculateSubstraction(double firstNumber, double secondNumber)
        {
            return firstNumber - secondNumber;
        }

        double CalculateMultiplication(double firstNumber, double secondNumber)
        {
            return firstNumber * secondNumber;
        }

        double CalculateDivision(double firstNumber, double secondNumber)
        {
            if(firstNumber != 0.0)
            {
                return firstNumber / secondNumber;
            }
            else
            {
                return double.NaN;
            }
        }   
    }
}

リファクタリングの結果、CalculateOperationMethodメソッドは各メソッドを呼び出すだけの機能に絞ることができました。
また、インプット/アウトプットに関してはCalculatingInformationInputを新しく作ることでインプット系をすべてこのメソッドで管理することができるようになりました。
四則演算をすべて同一のメソッドで行っていたCalculateOperationに関しては、それぞれ新しく生成された計算用のメソッドを呼び出すことで計算結果の責任を委譲することができました。
これにより計算フローを変更したくなった際にも改修が楽になりますね。

今回の例では簡単な内容であった為ありがたみが薄いですが、これが現場のソースコードになると絶大な威力を発揮します。
各メソッドは単一の機能にのみ責任を持つような設計は、簡潔で単純であり、それだけ保守、改修のコストも低くなります。

以上、KISSの観点を元に4つのコーディング例を示しました。
リファクタリングした後に見返すと、どれも比較的当たり前のことしかしていないことに気づくのではないでしょうか。
コーディングに慣れている方々からすれば当然の内容かもしれません。
しかし当然のことを当然のように実現することに大きな意味があるのです。 むしろ慣れている方は普段からどのようにしているか、どうすればよりシンプルにコーディングができるか教えてください。

まとめ

総評として、輪読ミッションは各々の解釈に基づいて発表するため、自分の解釈以外の角度で理解することができて勉強になりました。
また、上長や先輩方からも好評だったため、達成感も得ることができてハッピーハッピー。
内容についての補足、アドバイス、共感を頂けたので、圧倒的成長ができたと思います。

読んで感じたこと

もし新人プログラマが「プリンシプル・オブ・プログラミング」を読んだら

  • 考え方を鍛えることができる
    • プログラマは日々わかりやすさと格闘している
    • 絶対のベストソリューションはないので、各々が意識してやりやすい方法を模索
  • プログラミングや設計においてシンプルさの追求が肝要であることが分かる
    • 経験を積めば多少複雑でもなんとかなるけど、ソロのプロジェクトはほぼ存在しないので
    • 大抵コードは書く時間 < 読む時間

→圧倒的成長をすることができる
といった結果になりました。

今後のモチベーション

今後のモチベとしては、一日でも早く一人前にプログラムを書けるようになることが目標です。
早いうちからシンプルに書ける意識、癖を身に着けておくためにも本書を読んだのは影響力が大きいなと。
幸いな事に今年配属された新人3人でハッカソンも企画しているため、次はゴリゴリ書いていく方向で勉強したいと思います。
インプットの次はアウトプットを充実させなければ…
それについては、後日記事にまとめたいと思いますのでお楽しみに!

最後に、自分からこの本を手に取ったかは非常に怪しいなぁ…と思いましたので、機会を設けて頂いたのは大変ありがたかったです。
このようにプロダクト開発チームではメンバーの成長に積極的なので、この環境をどんどん活用して最強のエンジニアになりたいですね。

開発

~ecbeingでは、プログラミングの原理や法則を学び成長し続けるエンジニアを大募集中です!~ careers.ecbeing.tech