原著者Bora Kaşmer氏の許諾を得て翻訳・公開しています。
記事: Refactoring For Clean Code With Design Patterns
原文公開日: 2020年5月1日
画像出典: HOLY BATH
こんにちは。
今日はクリーンコードについてお話しします。クリーンコードは本当に必要でしょうか?コードがぐちゃぐちゃに見えたら、この先何が待っているのでしょうか?クリーンコードは、読みやすくシンプルであることだけを意味するのでしょうか?それともそれ以上のことを意味するのでしょうか?
結局の所、読みやすいコードと変更しやすいコードには違いがあるのです。
ー “Big” Dave Thomas
まず第一に、だれもそんなに時間を持っていません。上司はプロジェクトの完了を待っています。上司は詳細について気にしていません。お客さんの前では、自分自身はお肉屋さんと考えることができます。お客さんは上司であり、あなたはお肉屋さんです。お客さんは衛生面について気にしていません。彼はできるだけ早く肉を受け取りたいだけなのです。そのため上司はお客さんごとに手を洗ってほしくないのです。なぜでしょうか、時は金なりだからです。
最後に、もしあなたが手を洗わず、時間を浪費しないならば(この例とは違って、いつも手を洗ってくださいね、お願いします!)、1日の終わりには、あなたを除くみなさんが幸せになります :) しかし将来的には、お客さんは病気になり会社を訴えるでしょう。結局のところ、誰もハッピーにならないでしょう。上司は詳細を知りません。あなただけがリスクを知っているのです。そのため、全責任をとって、可能な限りコードを綺麗にしてください。
この記事の最後で、非常に厄介なAI アプリケーションコードをリファクタリングします。ですがまずは、3つのデザインパターンのストラテジを理解する必要があります。このストラテジは記事の最後にあるぐちゃぐちゃなコードのリファクタリングに使用します。もしかしたらほとんどの人はこれらについて既に知っているかもしれませんが、良いことを繰り返しても損はありません :) まとめると、次の2つのデザインパターンで最後の例のための準備をします。そして、ぐちゃぐちゃなAIアプリケーションで最後の1つを詳しく検証します。ビジネストレーニングのようなものです :)
繰り返されるコードを避けなければならない...
以下にあるC#コンソールアプリのコードには、複数のビジネスロジックがあることが分かります。これはS.O.L.I.Dの最初の原則に反しています。
単一責任の原則です。
クラスに変更がおきる理由は1つであるべきです。なぜかというと、読みやすく、テストしやすく、シンプルにするためです。コードを見てみましょう。各ロジックは独自のビジネスを持っています。CRM、Finance、Serviceです。
ソフトウェアはオールインワンキャンペーンではありません。分離されたプロセスです。もしCRMのプロセスを変更したい場合、このクラスを使用している全てのアプリケーションが、このアップデートに影響を受けます。そして、1つのビジネスの変更のために、他の処理を全てリビルドする必要があります。これは**"CRM"プロセスの変更のために、"Finance"、"Service"**は停止しなければならないことを意味しています。enumを使っているのがこのコードで唯一の良いところです :)
このコードで他のひどい臭いを感じませんか?将来、もう1つプロセスが必要になった時、このクラスを見つけて別のif文を追加しなければならないのです。それは非常識で、持続可能ではありません。もしこのクラスに別の10個の新しいロジックがやってくると、コードが読めなくなります。
using System;
namespace SolidStrategy
{
public enum ProcessType
{
Finance,
Service,
Crm
}
class Program
{
static void Main(string[] args)
{
ProcessCalculation(ProcessType.Crm);
}
public static void ProcessCalculation(ProcessType type)
{
try
{
if (type == ProcessType.Crm)
{
CallCrm("Update Crm DB");
}
else if (type == ProcessType.Finance)
{
CallFinance("Send Mail to Customers");
}
else if (type == ProcessType.Service)
{
CallService("Backup all Data");
}
}
catch
{
}
}
public static void CallCrm(string message)
{
Console.WriteLine($"Process Crm! :{message}");
}
public static void CallFinance(string message)
{
Console.WriteLine($"Process Finance! :{message}");
}
public static void CallService(string message)
{
Console.WriteLine($"Process Service! :{message}"); }
}
}
}
最初に、全てのプロセスを違うクラスに分割しましょう。そして全てのクラスは別のクラスもしくはインターフェースから継承しなければなりません。オブジェクト指向で裸のクラスは好ましくありません。
IBussineLogic :
全てのビジネスロジックは同じインターフェース(IBussinessLogic)から継承しなければなりません。そして全てのクラスは同じ"WorkProcess()"メソッドをオーバーライドしなければなりません。このデザインパターンを覚えていますか?
using System;
namespace SolidStrategy
{
public interface IBussinesLogic
{
void WorkProcess(string message);
}
}
Crm:
最初のビジネスクラスはCRMです。***"IBusinessLogic"***を継承しています。そのため "WorkProcess()" メソッドを実装しなければなりません。各"WorkProces()"メソッドは異なる親クラスに属しています。そのため、全てのメソッドは異なるロジックを持っています。
Crmクラス:
using System;
namespace SolidStrategy
{
public class Crm : IBussinesLogic
{
public void WorkProcess(string message)
{
Console.WriteLine($"Process Crm! :{message}");
}
}
}
Finance:
2つ目のビジネスクラスはFinanceです。CRMのように、"IBusinessLogic"を継承しています。そしてまた、異なるFinacialロジックのために***"WorkProcess()"***メソッドを実装する必要があります。
using System;
namespace SolidStrategy
{
public class Finance : IBussinesLogic
{
public void WorkProcess(string message)
{
Console.WriteLine($"Process Finance! :{message}");
}
}
}
Service:
最後のビジネスクラスはServiceです。他のビジネスクラスのように、 "IBusinessLogic" を継承しています。異なるServiceロジックのために "WorkProces()" メソッドを実装する必要があります。
using System;
namespace SolidStrategy
{
public class Service : IBussinesLogic
{
public void WorkProcess(string message)
{
Console.WriteLine($"Process Service! :{message}");
}
}
}
Process:
1つのコンテナ内でビジネスロジックを全てカバーする必要があります。なぜかというとお客さんのためであり、このケースでは実際には、他の開発者は1つのクラスにのみ興味をもつべきだからです。なぜならシンプルさが全てだからです。"もし新しいビジネスロジックが町にやってきたとしても、誰もメインのProcessのコードを変えなくてよいのです :)"
Processクラスはコンストラクタで "IBusinessLogic" を継承するクラスが必要です。
すべてのクラスに共通する特徴は何でしょうか?もちろん "WorkProcess()" メソッドです。Processクラスから"WorkProcess()"メソッドが呼ばれた時、コンストラクタから受け取った"IBusinessClass"のクラスによってロジックが異なります。
using System;
namespace SolidStrategy
{
public class Process
{
IBussinesLogic _bussines = null;
public Process(IBussinesLogic bussines)
=> _bussines = bussines;
public void WorkProcess(string message)
{
_bussines.WorkProcess(message);
}
}
}
Strategy デザインパターンが上記の質問の答えです。実行時にアルゴリズムの選択をできるようにする、振る舞いに関するデザインパターンです。このような状況で、コードを綺麗にシンプルにするためにこのパターンを使用しました。
この世界を少しでも良くしてみてください。
— Robert Baden-Powell
Program.cs
ぐちゃぐちゃなコードはこの数行のコードになってきています :)
新しいおもちゃで遊んでみましょう :)
シンプルさが全てです。より一層読みやすいコードとなります。しかしもっと重要なことがあります。
"他のコードを変更することなく、新しいロジックを簡単に加えることができるということです。そしてだれもその変更による影響を受けないのです。"
Program.cs/Main() :
using System;
namespace SolidStrategy
{
class Program
{
static void Main(string[] args)
{
Process process = new Process(new Crm());
process.WorkProcess("Update Crm DB");
}
}
}
結果の画面 :
メソッドの抽出
2つ目のシナリオはメソッドの抽出についてです。抽出は私の大好きなリファクタリング方法です。
メソッドの抽出を使うことで、既存のメソッドから新しいメソッドへコードの断片を移動します。そして、そのメソッドが何をしているのかについて名前をつけることを忘れないでください。このテクニックは複雑さから抜け出し、コードの読みやすさを向上させます。
"塊の中にあるバグを見つけることは干し草の中にある針を探すようなものです。塊を分割し、全てのエラーを1つ1つ見つけてください。そして塊を分割したら、それを共有できることを決して忘れないでください。"
ー Bora Kasmer
下記のコードを見てください。全てが一箇所にあり、全てのロジックが一緒に動いています。異なるロジックを全て見つけ、このメソッドから別のメソッドへ抽出します。もっと読みやすくシンプルにすることが目的です。
using System;
using System.Collections.Generic;
namespace ExtractMethod
{
enum Department
{
IT = 1,
HumanResource = 2,
System = 3,
Officer = 4
}
enum Gender
{
Male,
Female
}
class Person
{
public int Identity;
public String Name;
public double Salary;
public int Department;
public Gender Gender;
public Person(int _identity, string name, float _salary, Department _department, Gender _gender)
{
this.Identity = _identity;
this.Name = name;
this.Salary = _salary;
this.Department = (int)_department;
this.Gender = _gender;
}
}
class Program
{
static void Main(string[] args)
{
List<Person> listPerson = new List<Person>();
listPerson.AddRange(new List<Person>() {
new Person(1, "Test User1", 5500, Department.HumanResource,Gender.Male),
new Person(2, "Test User2", 6500, Department.Officer,Gender.Female),
new Person(3, "Test User3", 7500, Department.IT,Gender.Female)
});
CalculateSalaryIncrease(ref listPerson);
foreach (Person person in listPerson)
{
Console.WriteLine($"User Name:{person.Name} Sallary:{person.Salary} Departmant:{(Department)person.Department}");
}
}
public static void CalculateSalaryIncrease(ref List<Person> personList)
{
foreach (Person person in personList)
{
switch ((Department)person.Department)
{
case Department.IT:
{
person.Salary = person.Salary * 1.1;
break;
}
case Department.HumanResource:
{
person.Salary = person.Salary * 1.2;
break;
}
case Department.System:
{
person.Salary = person.Salary * 1;
break;
}
case Department.Officer:
{
person.Salary = person.Salary * 1.3;
break;
}
}
person.Name = person.Gender == Gender.Male ? $"Mr.{person.Name}" : $"Ms.{person.Name}";
}
}
}
}
***“CalculateSallaryIncrease()"***メソッドをチェックしてみましょう。このメソッドには3つのジョブがあります。
1-) 部署別の昇給率を探す:
このコードは、その人の部署に応じて昇給率を返しています。名前は重要です。名前からメソッドの目的が分かります。
public static double GetSalaryRatebyDepartment(Department department)
{
switch (department)
{
case Department.IT:
{
return 1.1;
}
case Department.HumanResource:
{
return 1.2;
}
case Department.System:
{
return 1;
}
case Department.Officer:
{
return 1.3;
}
}
return 1;
}
2-)上記のコードで見つけた新しい"昇給率"を使用して従業員の新しい給料を計算してみましょう
パラメータの順番はメソッドの名前から分かるようになっています。Calculate => New Sallary =>With Rate (salary, rate).
将来、1人1人の給料計算のために、このメソッドに新しいビジネスロジックを簡単に加えることができます。
public static double CalculateNewSallaryWithRate(double salary, double rate)
{
return salary * rate;
}
3-) 性別によって人の名前にMr.もしくはMs.のタグを加えます
なぜこのメソッドを分割したのでしょうか?なぜなら、将来誰かがどんな条件でも新しいタグを加えたいならば、余計な努力なしでこのメソッドに簡単に追加できるようにするべきです。
public static string AppendTagToNameByGender(string name, Gender gender)
{
return gender == Gender.Male ? $"Mr.{name}" : $"Ms.{name}";
}
新しい***“CalculateSalaryIncrease()”メソッドはこのようになりました。もっと読みやすく理解できるようにするために最も重要なことは、一層"短く"***することです。
public static void CalculateSalaryIncrease(ref List<Person> personList)
{
foreach (Person person in personList)
{
double sallaryRate = GetSalaryRatebyDepartment((Department)person.Department);
person.Salary = CalculateNewSallaryWithRate(person.Salary, sallaryRate);
person.Name = AppendTagToNameByGender(person.Name, person.Gender);
}
}
結果の画面 :
この記事の最後にしてメインの例です:
以下の例で作られている3つのコメント(Description1, Description2, Description3)が女性のものなのか男性のものなのか、具体的な分析ルールを使って理解しようとしています
もちろん、現実のシナリオではありません。しかし、それはクリーンコードを理解することに役立っています。単語ライブラリのグループを2つ持っています。その言葉は男性や女性が話しているときによく使う言葉です。発言されたコメントの中で、これらの単語のグループもしくは単語を絞り込むことで、話している人の性別が男性か女性かどうか決めようとします。
男性の場合、説明文の中でfootballもしくはcar の単語を探します。もし解釈の中にこれらの言葉が1つでも出てきたら、それを言っている人は男性であると受け止めます。
女性の場合、(motherもしくはbaby) もしくは (drawとcar) もしくは (rub とcar)を探します。もし解釈の中にこれらの言葉、もしくは言葉のグループが1つでも出てきたら、それを言っている人は女性と受け止めます。
このコードの悪いところは何か?
. このコードは説明文なしに理解することがとても難しいです。
. コードの可読性がひどいです。
. もし新しいルールがやってきたら、全ての条件を修正しなければなりません。
. しばらくすると、andとorの条件に従うことが不可能になります。
.最後に、このコードは人間が理解できるように書かれていません。コンピュータが理解できるよう書かれています :)
using System;
namespace Interpreter
{
class MainClass
{
public static void Main(string[] args)
{
string Description1 = "Although I like football matches very much, I don't usually watch.";
string Description2 = "I attach great importance to breast milk for my baby's health.";
string Description3 = "I hit the car on my way to the gym.";
bool expResult = false;
string word = "football";
string word2 = "car";
string word3 = "mother";
string word4 = "baby";
string word5 = "draw";
string word6 = "rub";
Console.Write(Description1 + " (Man):");
//football || car
expResult = Description1.ToLower().Contains(word.ToLower()) || Description1.ToLower().Contains(word2.ToLower());
Console.WriteLine(expResult);
//mother || baby , draw && car , rub && car
Console.Write(Description2 + "(Woman):");
expResult = ((Description2.ToLower().Contains(word3.ToLower()) || Description2.ToLower().Contains(word4.ToLower()))
|| (Description2.ToLower().Contains(word5.ToLower()) && Description2.ToLower().Contains(word2.ToLower()))
|| (Description2.ToLower().Contains(word6.ToLower()) && Description2.ToLower().Contains(word2.ToLower())));
Console.WriteLine(expResult);
//mother || baby , draw && car , rub && car
Console.Write(Description3 + "(Woman):");
expResult = ((Description3.ToLower().Contains(word3.ToLower()) || Description3.ToLower().Contains(word4.ToLower()))
|| (Description3.ToLower().Contains(word5.ToLower()) && Description3.ToLower().Contains(word2.ToLower()))
|| (Description3.ToLower().Contains(word6.ToLower()) && Description3.ToLower().Contains(word2.ToLower())));
Console.WriteLine(expResult);
Console.ReadLine();
}
}
}
Interpreterデザインパターン
このぐちゃぐちゃなものを綺麗にするために、2つのデザインパターンを使います。
そしてもちろん、このソリューションのメインのパターンは"Interpreterデザインパターン"です。
interpreterは振る舞いに関するデザインパターンです。インターフェースから実装されている言語の文法や表現を評価するために使用します。このアプリケーションでは、私達のexpressionはこれら3つの説明文です。このパターンは、特定の文脈を解釈するために、expressionインターフェースを使用しています。
Expressionインターフェースを作成します。その後、Expressionインターフェースを実装したAnd - Orクラスを作成します。"Or"Expressionと*"And"Expressionは、組み合わせたexpressionを作成するために使われます。そしてもちろん、 説明文での文脈の主なインタプリタとしての役割を定義した、“TerminalExpression”* クラスを作成します。このアプリケーションでは、*"CheckExpression"*と呼んでいます。
"将来、コメンテーターの性別を検出するための新しいルールがやってきたら、新しい種類のExpressionを作成しなければならないかもしれません。"
1-)Expressionインターフェースを作成しましょう: 他の全てのexpressionクラスはこの“Interpret()”メソッドを使用しなければなりません。
public interface Expression
{
bool Interpret(string content);
}
2-) CheckExpressionを作成します: コンストラクタから単語を取り出し、Interpretメソッドでパラメータとして受け取った内容(説明文)に、関連する単語が含まれていないかチェックします。これが最初のツールになります。そしてこれをどこでも使うことになるでしょう。
CheckExpressionクラス :
public class CheckExpression : Expression
{
private string word;
public CheckExpression(string _word)
{
this.word = _word;
}
public bool Interpret(string content)
{
return content.ToLower().Contains(word.ToLower());
}
}
3-) OrExpressionを作成します: これは2つ目のツールとなります。上記のExpressionクラスを使います。このメソッドには2つのexpressionクラスが必要です。このアプリケーションではexrepssionは***"単語"を意味しています。この説明文にこれらの単語が"1つ"***でも含まれているかどうかチェックします。)
OrExpressionクラス :
public class OrExpression : Expression
{
private Expression exp1;
private Expression exp2;
public OrExpression(Expression _exp1, Expression _exp2)
{
this.exp1 = _exp1;
this.exp2 = _exp2;
}
public bool Interpret(string content)
{
return (exp1.Interpret(content) || exp2.Interpret(content));
}
}
少し注意してみると、別のツールを使って新しいツールを作っていることが分かりますね!ロボットを使って新しいロボットを作っているようなものです :)
4-) AndExprssionを作ります: これが最後のツールになります。ここでも、2つのexpressionクラスが必要です。これらの単語の "両方" が説明文に含まれているかどうかチェックします。
AndExpressionクラス :
public class AndExpression : Expression
{
private Expression exp1;
private Expression exp2;
public AndExpression(Expression _exp1, Expression _exp2)
{
this.exp1 = _exp1;
this.exp2 = _exp2;
}
public bool Interpret(string content)
{
return (exp1.Interpret(content) && exp2.Interpret(content));
}
}
これら3つのツールを作って何をするのでしょうか?
1-) 塊のコードを分けました。既存のメソッドから新しいメソッド(AndExpression、OrExpression)へ2つのコードの断片を移動しました。もう少し作業をして、これらのメソッドから新しいメソッド(CheckExpression)へ1つのコードの断片を移動しました。覚えていますか?
記事の最初で話した"メソッドの抽出"と呼んでいます。
2-) “AndExpressionとOrExpression”クラスに注目してください。どちらも"Expression"インターフェースを継承しています。どちらも同じ“Interpret()”メソッドを持っています。以下のコードを見てください。“getFemailExpression()”はExpressionのListを持っています。"Expression"インターフェースを継承したどんなクラスでも受け取ることができます。“AndExpression,” “OrExpression” のように。これは、*“getFemailExpressions()”***が実行時にアルゴリズムを選択することを意味しています。覚えていますか?
記事の上の方で話した"Strategyデザインパターン"と呼んでいます。
InterpretPatternクラス :
. CheckExpression: これは単語です。
. OrExpression — AndExpression: コメンテーターの性別を決めるExpressionです。性別を決めるためには1つ以上の “CheckExpression” が必要です。
. Interpret(): 全てのExpressionはこのメソッドを実装しなければなりません。パラメータとして内容(説明文)を受け取ります。そして男性か女性か性別を決めます。
3つのツールは全てこのクラスで使用されるために作成されています。“AndExpression”、“Expression”、そして“OrExpression”です。 ここには2つの状況があります。男性もしくは女性を決定するルールです。それらはExpressionもしくはExpressionのListとして集められ、返されます。
getMaleExpression(): OrExpressionを返します。各expressionは単語であり、OrExpressionクラスにこの2つのパラメータが与えられます。各OrExpressionは性別を決めるルールです。
getFemailExpression(): ExpressionのListを返します。女性を決めるための3つのExpressionルールです。2つのAndExpressionと1つのOrExpressionです。いずれも6つのexpression(単語)をパラメータとして受け取ります。
public class InterpretPattern
{
public static Expression getMaleExpression()
{
Expression futbol = new CheckExpression("football");
Expression araba = new CheckExpression("car");
return new OrExpression(futbol, araba);
}
public static List<Expression> getFemailExpressions()
{
List<Expression> ListExpression = new List<Expression>();
Expression mother = new CheckExpression("mother");
Expression baby = new CheckExpression("baby");
Expression rub = new CheckExpression("rub");
Expression draw = new CheckExpression("draw");
Expression car = new CheckExpression("car");
ListExpression.Add(new OrExpression(mother, baby));
ListExpression.Add(new AndExpression(rub, car));
ListExpression.Add(new AndExpression(draw, car));
return ListExpression;
}
}
全てがパズルの一部のように見えるでしょう。いずれも連動していて、小さいピースは大きいピースを形成し、大きいピースは写真を形成します。
UnsplashのMarkus Winklerによる写真
以下のコードを見てください。一層クリアになりました。実装は簡単です。ビジネスロジックを知る必要はありません。"女性を決めるためにどの単語が内容に含まれていなければならないかということは、あなたの関心事ではありません。"
新しいルールもしくは新しい単語がビジネスロジックにやってきても、全てのコードを変更する必要はありません。これがOOPプログラミングの本質です。
フォーメーションツリー :
“Expressions =>CheckExpression =>OrExpression&AndExpression =>getFemailExpressions&getMaleExpression”
ビジネスロジックに沿ってルールを1つ1つ実行していく場合、もしルールの1つが特定条件をパスしたら、全ての処理を停止して"true"を返します。
using System;
namespace Interpreter
{
class MainClass
{
public static void Main(string[] args)
{
string Description1 = "Although I like football matches very much, I don't usually watch.";
string Description2 = "I attach great importance to breast milk for my baby's health.";
string Description3 = "I hit the car on my way to the gym.";
Console.Write(Description1 + " (Man):");
Console.WriteLine(InterpretPattern.getMaleExpression().Interpret(Description1));
bool expResult = false;
Console.Write(Description2 + "(Woman):");
foreach (Expression exp in InterpretPattern.getFemailExpressions())
{
if (exp.Interpret(Description2)) { expResult = true; break; }
}
Console.WriteLine(expResult);
bool expResult2 = false;
Console.Write(Description3 + "(Woman):");
foreach (Expression exp in InterpretPattern.getFemailExpressions())
{
if (exp.Interpret(Description3)) { expResult2 = true; break; }
}
Console.WriteLine(expResult2);
Console.ReadLine();
}
}
}
結果の画面 :
この記事では、デザインパターンを使うことで、読めないぐちゃぐちゃなコードを明示的で明快なコードにどのように変換するかについて話しました。いくつかのケースでは、1つのデザインパターンのみでは問題を解決できないこともあります。その場合、解決のために複数のデザインパターンが望ましいです。
OOPプログラミングとデザインパターンはメンテナンス(変更とテスト)を簡単にするためにあなたのコードを最適化し、拡張性と柔軟性をもたせます。
クイックヒント: 男性のコメンテーターを特定するための新しいルールが“getMaleExpression()”にやってくることを想像してみましょう。このシナリオのために、3つのexpression(単語)は内容(説明文)に含まれなければなりません。これはまったく新しいものです。
以下のような新しいExpressionの種類を作成するだけで十分です。それは “And3Expression” と名付けられています。Expressionインターフェースを実装し、3つのExpression(単語)を作成します。最後に、“Interpret()” メソッド上で、内容(説明文)にこれら3つの単語が含まれているかどうかチェックします。それだけです。どこかで何かを変える必要はありません。新しい“And3Expression”クラスを加えて、“getMaleExpression()” で使うのみです。これがOOPプログラミングの力です。
public class And3Expression : Expression
{
//bag,shoe,hairdresser
private Expression exp1;
private Expression exp2;
private Expression exp3;
public And3Expression(Expression _exp1, Expression _exp2, Expression _exp3)
{
this.exp1 = _exp1;
this.exp2 = _exp2;
this.exp3 = _exp3;
}
public bool Interpret(string content)
{
return (exp1.Interpret(content) && exp2.Interpret(content) && exp3.Interpret(content));
}
}
"まず、ここまで読んでくれた方はお待ちいただきありがとうございました。私のブログに来てくれることを歓迎します!"
Source Codes: https://github.com/borakasmer/CleanCode
Sources: refactoring.com, geeksforgeeks.org, en.wikipedia.org
翻訳を終えて
翻訳を行うのと同時に、StrategyパターンやInterpreterパターンといったデザインパターンの勉強にもなりました。
クラスが持つべき役割、関心事を分離し、小さくまとめることでクリーンコードになるのですね。
誤訳等ありましたらご指摘をお願いします。