良いデザインまたは悪いデザインの概念は相対的です。同時に、いくつかのプログラミング標準があり、ほとんどの場合、有効性、保守性、およびテスト性が保証されています。たとえば、オブジェクト指向言語では、これはカプセル化、継承、およびポリモーフィズムの使用です。多くの場合、状況に応じてアプリケーション設計にプラスまたはマイナスの影響を与える一連のデザインパターンがあります。一方、反対のことがあり、それに続いて問題の設計につながることがあります。
この設計には通常、次のインジケーターがあります(一度に1つまたは複数):
- 剛性(単純な変更は多くの場所に影響するため、コードを変更することは困難です);
- 不動(コードを他のプログラムで使用できるモジュールに分割するのは複雑です);
- 粘度(コードの開発やテストは非常に困難です);
- 不必要な複雑さ(コードには未使用の機能があります)。
- 不必要な繰り返し(コピー/貼り付け);
- 読みやすさが悪い(コードが何のために設計されているのかを理解し、維持するのが難しい)
- 脆弱性(小さな変更でも機能が壊れやすい)
問題の設計を回避したり、その使用によって起こりうる結果を予測したりするには、これらの機能を理解して区別できる必要があります。これらの指標は、Robert Martinの著書「AgilePrinciples、Patterns、and Practices in C#」で説明されています。ただし、この記事と他のレビュー記事には簡単な説明があり、コード例はありません。
各機能に存在するこの欠点を解消します。
剛性
すでに述べたように、厳密なコードは、たとえ小さなものであっても、変更するのは困難です。コードが頻繁にまたはまったく変更されない場合、これは問題にならない可能性があります。したがって、コードは非常に優れていることがわかります。ただし、コードを変更する必要があり、それが難しい場合は、機能しても問題になります。
一般的な剛性のケースの1つは、抽象化(インターフェイス、基本クラスなど)を使用する代わりに、クラスタイプを明示的に指定することです。以下に、コードの例を示します。
class A
{
B _b;
public A()
{
_b = new B();
}
public void Foo()
{
// Do some custom logic.
_b.DoSomething();
// Do some custom logic.
}
}
class B
{
public void DoSomething()
{
// Do something
}
} ここで、クラスAはクラスBに大きく依存しています。したがって、将来、クラスBの代わりに別のクラスを使用する必要がある場合は、クラスAを変更する必要があり、再テストされることになります。さらに、クラスBが他のクラスに影響を与える場合、状況は非常に複雑になります。
回避策は、クラスAのコンストラクターを介してIComponentインターフェースを導入する抽象化です。この場合、特定のクラスÂに依存せず、IComponentインターフェースのみに依存します。次に、СlassВはIComponentインターフェイスを実装する必要があります。
interface IComponent
{
void DoSomething();
}
class A
{
IComponent _component;
public A(IComponent component)
{
_component = component;
}
void Foo()
{
// Do some custom logic.
_component.DoSomething();
// Do some custom logic.
}
}
class B : IComponent
{
void DoSomething()
{
// Do something
}
} 具体的な例を挙げましょう。情報をログに記録する一連のクラス(ProductManagerとConsumer)があると想定します。彼らの仕事は、データベースに製品を保存し、それに応じて注文することです。どちらのクラスも、関連するイベントをログに記録します。最初にファイルへのログインがあったと想像してください。これを行うために、FileLoggerクラスが使用されました。さらに、クラスは異なるモジュール(アセンブリ)に配置されていました。
// Module 1 (Client)
static void Main()
{
var product = new Product("milk");
var productManager = new ProductManager();
productManager.AddProduct(product);
var consumer = new Consumer();
consumer.PurchaseProduct(product.Name);
}
// Module 2 (Business logic)
public class ProductManager
{
private readonly FileLogger _logger = new FileLogger();
public void AddProduct(Product product)
{
// Add the product to the database.
_logger.Log("The product is added.");
}
}
public class Consumer
{
private readonly FileLogger _logger = new FileLogger();
public void PurchaseProduct(string product)
{
// Purchase the product.
_logger.Log("The product is purchased.");
}
}
public class Product
{
public string Name { get; private set; }
public Product(string name)
{
Name = name;
}
}
// Module 3 (Logger implementation)
public class FileLogger
{
const string FileName = "log.txt";
public void Log(string message)
{
// Write the message to the file.
}
} 最初はファイルだけを使用するだけで十分であり、次にデータベースやクラウドベースのデータ収集およびストレージサービスなどの他のリポジトリにログインする必要が生じた場合は、ビジネスロジックのすべてのクラスを変更する必要があります。 FileLoggerを使用するモジュール(モジュール2)。結局のところ、これは難しいことが判明する可能性があります。この問題を解決するために、以下に示すように、ロガーと連携するための抽象的なインターフェースを導入できます。
// Module 1 (Client)
static void Main()
{
var logger = new FileLogger();
var product = new Product("milk");
var productManager = new ProductManager(logger);
productManager.AddProduct(product);
var consumer = new Consumer(logger);
consumer.PurchaseProduct(product.Name);
}
// Module 2 (Business logic)
class ProductManager
{
private readonly ILogger _logger;
public ProductManager(ILogger logger)
{
_logger = logger;
}
public void AddProduct(Product product)
{
// Add the product to the database.
_logger.Log("The product is added.");
}
}
public class Consumer
{
private readonly ILogger _logger;
public Consumer(ILogger logger)
{
_logger = logger;
}
public void PurchaseProduct(string product)
{
// Purchase the product.
_logger.Log("The product is purchased.");
}
}
public class Product
{
public string Name { get; private set; }
public Product(string name)
{
Name = name;
}
}
// Module 3 (interfaces)
public interface ILogger
{
void Log(string message);
}
// Module 4 (Logger implementation)
public class FileLogger : ILogger
{
const string FileName = "log.txt";
public virtual void Log(string message)
{
// Write the message to the file.
}
} この場合、ロガーの種類を変更するときは、クライアントコード(メイン)を変更するだけで十分です。これにより、ロガーが初期化され、ProductManagerとConsumerのコンストラクターに追加されます。したがって、必要に応じてロガータイプを変更することでビジネスロジックのクラスを閉じました。
使用されているクラスへの直接リンクに加えて、コードを変更するときに問題が発生する可能性のある他のバリアントの剛性を監視できます。それらの無限のセットが存在する可能性があります。ただし、別の例を提供しようとします。コンソールに幾何学模様の領域を表示するコードがあると仮定します。
static void Main()
{
var rectangle = new Rectangle() { W = 3, H = 5 };
var circle = new Circle() { R = 7 };
var shapes = new Shape[] { rectangle, circle };
ShapeHelper.ReportShapesSize(shapes);
}
class ShapeHelper
{
private static double GetShapeArea(Shape shape)
{
if (shape is Rectangle)
{
return ((Rectangle)shape).W * ((Rectangle)shape).H;
}
if (shape is Circle)
{
return 2 * Math.PI * ((Circle)shape).R * ((Circle)shape).R;
}
throw new InvalidOperationException("Not supported shape");
}
public static void ReportShapesSize(Shape[] shapes)
{
foreach(Shape shape in shapes)
{
if (shape is Rectangle)
{
double area = GetShapeArea(shape);
Console.WriteLine($"Rectangle's area is {area}");
}
if (shape is Circle)
{
double area = GetShapeArea(shape);
Console.WriteLine($"Circle's area is {area}");
}
}
}
}
public class Shape
{ }
public class Rectangle : Shape
{
public double W { get; set; }
public double H { get; set; }
}
public class Circle : Shape
{
public double R { get; set; }
} ご覧のとおり、新しいパターンを追加するときは、ShapeHelperクラスのメソッドを変更する必要があります。オプションの1つは、以下に示すように、幾何学的パターンのクラス(長方形と円)でレンダリングのアルゴリズムを渡すことです。このようにして、対応するクラスの関連するロジックを分離し、コンソールに情報を表示する前にShapeHelperクラスの責任を軽減します。
static void Main()
{
var rectangle = new Rectangle() { W = 3, H = 5 };
var circle = new Circle() { R = 7 };
var shapes = new Shape[]() { rectangle, circle };
ShapeHelper.ReportShapesSize(shapes);
}
class ShapeHelper
{
public static void ReportShapesSize(Shape[] shapes)
{
foreach(Shape shape in shapes)
{
shape.Report();
}
}
}
public abstract class Shape
{
public abstract void Report();
}
public class Rectangle : Shape
{
public double W { get; set; }
public double H { get; set; }
public override void Report()
{
double area = W * H;
Console.WriteLine($"Rectangle's area is {area}");
}
}
public class Circle : Shape
{
public double R { get; set; }
public override void Report()
{
double area = 2 * Math.PI * R * R;
Console.WriteLine($"Circle's area is {area}");
}
} その結果、継承とポリモーフィズムを使用して新しいタイプのパターンを追加する変更について、ShapeHelperクラスを実際に閉じました。
不動
コードを再利用可能なモジュールに分割するときに、不動を監視できます。その結果、プロジェクトの開発と競争力が失われる可能性があります。
例として、コード全体が実行可能アプリケーションファイル(.exe)に実装され、ビジネスロジックが個別のモジュールまたはクラスに組み込まれないように設計されたデスクトッププログラムについて考えます。その後、開発者は次のビジネス要件に直面しました。
- ユーザーインターフェイスをWebアプリケーションに変えて変更するには;
- プログラムの機能を、サードパーティのクライアントが独自のアプリケーションで使用できる一連のWebサービスとして公開するため。
この場合、コード全体が実行可能モジュールにあるため、これらの要件を満たすことは困難です。
下の写真は、このインジケーターがないものとは対照的な、動かないデザインの例を示しています。それらは点線で区切られています。ご覧のとおり、再利用可能なモジュール(Logic)でのコードの割り当て、およびWebサービスのレベルでの機能の公開により、さまざまなクライアントアプリケーション(App)でコードを使用できるようになります。これは間違いなくメリットです。
不動はモノリシックデザインとも呼ばれます。それをコードの小さくて便利な単位に分割することは困難です。この問題をどのように回避できますか?設計段階では、この機能またはその機能を他のシステムで使用する可能性を検討することをお勧めします。再利用が期待されるコードは、別々のモジュールとクラスに配置するのが最適です。
粘度
2つのタイプがあります:
- 現像粘度
- 環境粘度
選択したアプリケーションの設計に従おうとすると、現像粘度を確認できます。これは、開発のより簡単な方法があるのに、プログラマーがあまりにも多くの要件を満たす必要がある場合に発生する可能性があります。さらに、組み立て、展開、およびテストのプロセスが効果的でない場合に、現像粘度を確認できます。
簡単な例として、他のコンポーネント(モジュール2およびモジュール3)で使用される別のモジュール(モジュール1)に(設計により)配置される定数を使用した作業を検討できます。
// Module 1 (Constants)
static class Constants
{
public const decimal MaxSalary = 100M;
public const int MaxNumberOfProducts = 100;
}
// Finance Module
#using Module1
static class FinanceHelper
{
public static bool ApproveSalary(decimal salary)
{
return salary <= Constants.MaxSalary;
}
}
// Marketing Module
#using Module1
class ProductManager
{
public void MakeOrder()
{
int productsNumber = 0;
while(productsNumber++ <= Constants.MaxNumberOfProducts)
{
// Purchase some product
}
}
} なんらかの理由で組み立てに時間がかかると、開発者は組み立てが完了するまで待つのが難しくなります。さらに、定数モジュールには、ビジネスロジックのさまざまな部分(財務モジュールとマーケティングモジュール)に属する混合エンティティが含まれていることに注意してください。そのため、定数モジュールは互いに独立した理由で頻繁に変更される可能性があり、変更の同期などの追加の問題が発生する可能性があります。
これらすべてが開発プロセスを遅くし、プログラマーにストレスを与える可能性があります。粘性の低い設計の変形は、ビジネスロジックの対応するモジュール用に1つずつ、個別の定数モジュールを作成するか、個別のモジュールを使用せずに定数を適切な場所に渡すことです。
環境の粘性の例としては、リモートクライアント仮想マシンでのアプリケーションの開発とテストがあります。インターネット接続が遅いためにこのワークフローが耐えられなくなることがあるため、開発者は記述されたコードの統合テストを体系的に無視できます。これにより、この機能を使用するときにクライアント側でバグが発生する可能性があります。
不必要な複雑さ
この場合、デザインには実際には未使用の機能があります。この事実は、プログラムのサポートとメンテナンスを複雑にするだけでなく、開発とテストの時間を増やす可能性があります。たとえば、データベースからいくつかのデータを読み取る必要があるプログラムについて考えてみます。これを行うために、別のコンポーネントで使用されるDataManagerコンポーネントが作成されました。
class DataManager
{
object[] GetData()
{
// Retrieve and return data
}
} 開発者がDataManagerに新しいメソッドを追加してデータベースにデータを書き込む場合(WriteData)、これは将来使用される可能性が低いため、不必要な複雑さもあります。
もう1つの例は、あらゆる目的のためのインターフェースです。たとえば、文字列タイプのオブジェクトを受け入れる単一のProcessメソッドを使用したインターフェイスを検討します。
interface IProcessor
{
void Process(string message);
} タスクが明確に定義された構造を持つ特定のタイプのメッセージを処理することである場合、開発者にこの文字列を毎回特定のメッセージタイプに逆シリアル化させるよりも、厳密に型指定されたインターフェイスを作成する方が簡単です。
これがまったく必要ない場合にデザインパターンを使いすぎると、粘度のデザインにもつながる可能性があります。
未使用の可能性のあるコードを書くことに時間を無駄にするのはなぜですか? QAがこのコードをテストする場合があります。これは、実際に公開されており、サードパーティのクライアントが使用できるようになっているためです。これはまた、リリース時間を延期します。将来の機能を含めることは、その可能な利益がその開発とテストのコストを超える場合にのみ価値があります。
不必要な繰り返し
おそらく、ほとんどの開発者は、同じロジックまたはコードを複数コピーすることで構成されるこの機能に直面したか、遭遇するでしょう。主な脅威は、コードを変更する際のこのコードの脆弱性です。ある場所で何かを修正することにより、別の場所でこれを行うのを忘れる可能性があります。また、コードにこの機能が含まれていない場合に比べて、変更に時間がかかります。
不必要な繰り返しは、開発者の過失、およびこれを行うよりもコードを繰り返さないことがはるかに困難でリスクが高い設計の剛性/脆弱性が原因である可能性があります。ただし、いずれの場合も、再現性はお勧めできません。コードを絶えず改善し、再利用可能な部分を共通のメソッドやクラスに渡す必要があります。
読みやすさが悪い
コードを読んだり、コードが何のために作成されたのかを理解するのが難しい場合は、この機能を監視できます。読みやすさが低下する理由は、コード実行の要件(構文、変数、クラス)、複雑な実装ロジックなどに準拠していない可能性があります。
以下に、ブール変数を使用してメソッドを実装する、読みにくいコードの例を示します。
void Process_true_false(string trueorfalsevalue)
{
if (trueorfalsevalue.ToString().Length == 4)
{
// That means trueorfalsevalue is probably "true". Do something here.
}
else if (trueorfalsevalue.ToString().Length == 5)
{
// That means trueorfalsevalue is probably "false". Do something here.
}
else
{
throw new Exception("not true of false. that's not nice. return.")
}
} ここでは、いくつかの問題の概要を説明します。まず、メソッドと変数の名前は、一般的に受け入れられている規則に準拠していません。第二に、メソッドの実装は最善ではありません。
おそらく、文字列ではなくブール値を取る価値があります。ただし、文字列の長さを決定する方法を使用するよりも、メソッドの最初でブール値に変換することをお勧めします。
第三に、例外のテキストは公式のスタイルに対応していません。そのようなテキストを読むと、コードがアマチュアによって作成されたように感じることがあります(それでも、問題点があるかもしれません)。ブール値をとる場合、メソッドは次のように書き直すことができます。
public void Process(bool value)
{
if (value)
{
// Do something.
}
else
{
// Do something.
}
} それでも文字列を取得する必要がある場合のリファクタリングの別の例を次に示します。
public void Process(string value)
{
bool bValue = false;
if (!bool.TryParse(value, out bValue))
{
throw new ArgumentException($"The {value} is not boolean");
}
if (bValue)
{
// Do something.
}
else
{
// Do something.
}
} たとえば、メンテナンスとクローン作成によって複数のバグが発生する場合は、読みにくいコードを使用してリファクタリングを実行することをお勧めします。
脆弱性
プログラムの脆弱性は、変更時に簡単にクラッシュする可能性があることを意味します。クラッシュには、コンパイルエラーとランタイムエラーの2種類があります。最初のものは剛性の裏側にすることができます。後者はクライアント側で発生するため、最も危険です。したがって、それらは脆弱性の指標です。
間違いなく、指標は相対的です。誰かがコードを非常に注意深く修正し、クラッシュの可能性は非常に低いですが、他の人は急いで不注意にこれを行います。それでも、同じユーザーの異なるコードは、異なる量のエラーを引き起こす可能性があります。おそらく、コンパイル段階ではなく、コードを理解し、プログラムの実行時間に依存することが難しいほど、コードは脆弱であると言えます。
さらに、変更されない機能がクラッシュすることがよくあります。さまざまなコンポーネントのロジックの高い結合に悩まされる可能性があります。
特定の例を考えてみましょう。ここでは、特定のリソース(resourceUriとして定義)にアクセスするための特定の役割(ロールパラメーターとして定義)を持つユーザー承認のロジックが静的メソッドにあります。
static void Main()
{
if (Helper.Authorize(1, "/pictures"))
{
Console.WriteLine("Authorized");
}
}
class Helper
{
public static bool Authorize(int roleId, string resourceUri)
{
if (roleId == 1 || roleId == 10)
{
if (resourceUri == "/pictures")
{
return true;
}
}
if (roleId == 1 || roleId == 2 && resourceUri == "/admin")
{
return true;
}
return false;
}
} ご覧のとおり、ロジックは複雑です。新しい役割とリソースを追加すると、簡単に壊れてしまうことは明らかです。その結果、特定の役割がリソースへのアクセスを取得または喪失する可能性があります。以下に示すように、リソース識別子とサポートされている役割のリストを内部的に格納するResourceクラスを作成すると、脆弱性が減少します。
static void Main()
{
var picturesResource = new Resource() { Uri = "/pictures" };
picturesResource.AddRole(1);
if (picturesResource.IsAvailable(1))
{
Console.WriteLine("Authorized");
}
}
class Resource
{
private List<int> _roles = new List<int>();
public string Uri { get; set; }
public void AddRole(int roleId)
{
_roles.Add(roleId);
}
public void RemoveRole(int roleId)
{
_roles.Remove(roleId);
}
public bool IsAvailable(int roleId)
{
return _roles.Contains(roleId);
}
} この場合、新しいリソースと役割を追加するために、承認ロジックコードを変更する必要はまったくありません。つまり、実際には何も壊すことはありません。
ランタイムエラーをキャッチするのに役立つものは何ですか?答えは、手動、自動、および単体テストです。テストプロセスが適切に構成されているほど、脆弱なコードがクライアント側で発生する可能性が高くなります。
多くの場合、脆弱性は、剛性、読みやすさの悪さ、不必要な繰り返しなど、デザインの悪い他の識別子の裏側です。
結論
悪いデザインの主な識別子の概要と説明を試みました。それらのいくつかは相互依存しています。デザインの問題が必ずしも必然的に困難につながるとは限らないことを理解する必要があります。それらが発生する可能性があることを指摘するだけです。これらの識別子が監視されないほど、この確率は低くなります。