![見出し画像](https://assets.st-note.com/production/uploads/images/136508760/rectangle_large_type_2_ecda898e1b262d99e4c7900b98ac3622.png?width=1200)
C#初心者を卒業しよう(第6回)Single-responsibility principle
はじめに
今回は SOLID 原則の「S」Single-responsibility principle (単一責務の原則)に従ってサンプルプログラムをリファクタリングしていきます。
今までのサンプルプログラムでは、DistanceService クラスが多くを受け持ちすぎていたのです。今回は DistanceService クラスから Console 入出力を分離します。
解説
変換種別の入力を分離する
InputConvertNumber というクラスを新しく作成し、変換種別の入力をこのクラスの ReadConvertNumber() メソッドに移します。
public class InputConvertNumber
{
public string? ReadConvertNumber()
{
Console.WriteLine("Please input convert number.");
foreach (var value in Const.CONVERT_VALUE_DIC.Values)
{
Console.WriteLine(value.ConvertMessage);
}
return Console.ReadLine();
}
}
このクラスの ReadConvertNumber() メソッドを呼び出します。
protected virtual ConvertNumber InputConvertNumber()
{
var inputConvertNumber =
(InputConvertNumber?)serviceProvider.GetService(typeof(InputConvertNumber)) ??
throw new NotImplementedException(
"Faild to create 'InputConvertNumber' object.");
if (Enum.TryParse(
typeof(ConvertNumber),
inputConvertNumber.ReadConvertNumber(),
out var convertNumber))
{
if (Enum.IsDefined(typeof(ConvertNumber), convertNumber))
{
return (ConvertNumber)convertNumber;
}
throw new IndexOutOfRangeException(
$"Input is out of range. {(int)convertNumber}");
}
throw new FormatException("Invalid input convert number.");
}
DI コンテナに登録します。
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddTransient<DistanceService>()
.AddScoped<InputConvertNumber>()
.AddScoped<ToYards>()
.AddScoped<IDistance, ToYards>()
.AddScoped<ToMeters>()
.AddScoped<IDistance, ToMeters>();
距離の入力を分離する
同様にして距離の入力も分離します。
まずは、InputDistance クラスを作成して、距離の入力をこのクラスの ReadDistance() メソッドに分離します。
public class InputDistance
{
public string? ReadDistance()
{
Console.WriteLine("Please input distance.");
return Console.ReadLine();
}
}
このクラスの ReadDistance() メソッドを呼び出します。
protected virtual double InputDistance()
{
var inputDistance =
(InputDistance?)serviceProvider.GetService(typeof(InputDistance)) ??
throw new NotImplementedException(
"Faild to create 'InputDistance' object.");
if (double.TryParse(inputDistance.ReadDistance(), out var distance))
{
return distance;
}
throw new FormatException("Invalid input distance.");
}
DI コンテナに登録します。
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddTransient<DistanceService>()
.AddScoped<InputConvertNumber>()
.AddScoped<InputDistance>()
.AddScoped<ToYards>()
.AddScoped<IDistance, ToYards>()
.AddScoped<ToMeters>()
.AddScoped<IDistance, ToMeters>();
答えの出力を分離する
同様にして答えの出力も分離します。
まずは、OutputAnswer クラスを作成して、答えの出力をこのクラスの WriteAnswer() メソッドに分離します。
public class OutputAnswer
{
public void WriteAnswer(string message)
{
Console.WriteLine(message);
}
}
このクラスの WriteAnswer() メソッドを呼び出します。
protected virtual void OutputAnswer(IDistance distance)
{
var outputMessage = distance.CreateAnswerMessage();
var outputAnswer =
(OutputAnswer?)serviceProvider.GetService(typeof(OutputAnswer)) ??
throw new NotImplementedException(
"Faild to create 'OutputAnswer' object.");
outputAnswer.WriteAnswer(outputMessage);
}
DI コンテナに登録します。
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddTransient<DistanceService>()
.AddScoped<InputConvertNumber>()
.AddScoped<InputDistance>()
.AddScoped<OutputAnswer>()
.AddScoped<ToYards>()
.AddScoped<IDistance, ToYards>()
.AddScoped<ToMeters>()
.AddScoped<IDistance, ToMeters>();
プログラム全体
Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddTransient<DistanceService>()
.AddScoped<InputConvertNumber>()
.AddScoped<InputDistance>()
.AddScoped<OutputAnswer>()
.AddScoped<ToYards>()
.AddScoped<IDistance, ToYards>()
.AddScoped<ToMeters>()
.AddScoped<IDistance, ToMeters>();
using IHost host = builder.Build();
var service = host.Services.GetService<DistanceService>() ??
throw new NotImplementedException(
"Faild to create 'DistanceService' object.");
service.Run();
public enum ConvertNumber
{
ToYards = 1,
ToMeters = 2,
}
public class ConvertValues
{
public Type ConvertType { get; }
public string ConvertMessage { get; }
public ConvertValues(Type convertType, string convertMessage)
{
ConvertType = convertType;
ConvertMessage = convertMessage;
}
}
public class Const
{
public static readonly double METER_TO_YARD = 1.0936133d;
public static readonly IReadOnlyDictionary<ConvertNumber, ConvertValues> CONVERT_VALUE_DIC =
new Dictionary<ConvertNumber, ConvertValues>
{
{
ConvertNumber.ToYards,
new ConvertValues(
typeof(ToYards),
$"{(int)ConvertNumber.ToYards} : Convert meters to yards.")
},
{
ConvertNumber.ToMeters,
new ConvertValues(
typeof(ToMeters),
$"{(int)ConvertNumber.ToMeters} : Convert yards to meters.")
},
};
}
public interface IDistance
{
void Calculate(double distance);
string CreateAnswerMessage();
}
public class DistanceService(IServiceProvider serviceProvider)
{
public void Run()
{
var convertNumber = InputConvertNumber();
var distanceObject =
Create(Const.CONVERT_VALUE_DIC[convertNumber].ConvertType) ??
throw new NotImplementedException(
"Faild to create 'IDistance' object.");
var distance = InputDistance();
distanceObject.Calculate(distance);
OutputAnswer(distanceObject);
}
protected virtual ConvertNumber InputConvertNumber()
{
var inputConvertNumber =
(InputConvertNumber?)serviceProvider.GetService(typeof(InputConvertNumber)) ??
throw new NotImplementedException(
"Faild to create 'InputConvertNumber' object.");
if (Enum.TryParse(
typeof(ConvertNumber),
inputConvertNumber.ReadConvertNumber(),
out var convertNumber))
{
if (Enum.IsDefined(typeof(ConvertNumber), convertNumber))
{
return (ConvertNumber)convertNumber;
}
throw new IndexOutOfRangeException(
$"Input is out of range. {(int)convertNumber}");
}
throw new FormatException("Invalid input convert number.");
}
protected virtual IDistance? Create(Type convertType) =>
(IDistance?)serviceProvider.GetService(convertType);
protected virtual double InputDistance()
{
var inputDistance =
(InputDistance?)serviceProvider.GetService(typeof(InputDistance)) ??
throw new NotImplementedException(
"Faild to create 'InputDistance' object.");
if (double.TryParse(inputDistance.ReadDistance(), out var distance))
{
return distance;
}
throw new FormatException("Invalid input distance.");
}
protected virtual void OutputAnswer(IDistance distance)
{
var outputMessage = distance.CreateAnswerMessage();
var outputAnswer =
(OutputAnswer?)serviceProvider.GetService(typeof(OutputAnswer)) ??
throw new NotImplementedException(
"Faild to create 'OutputAnswer' object.");
outputAnswer.WriteAnswer(outputMessage);
}
}
public class InputConvertNumber
{
public string? ReadConvertNumber()
{
Console.WriteLine("Please input convert number.");
foreach (var value in Const.CONVERT_VALUE_DIC.Values)
{
Console.WriteLine(value.ConvertMessage);
}
return Console.ReadLine();
}
}
public class InputDistance
{
public string? ReadDistance()
{
Console.WriteLine("Please input distance.");
return Console.ReadLine();
}
}
public class ToYards : IDistance
{
private double _yards;
private double _meters;
public void Calculate(double meters)
{
_meters = meters;
_yards = meters * Const.METER_TO_YARD;
}
public string CreateAnswerMessage()
{
return $"{_meters} meters is {_yards} yards.";
}
}
public class ToMeters : IDistance
{
private double _yards;
private double _meters;
public void Calculate(double yards)
{
_yards = yards;
_meters = yards / Const.METER_TO_YARD;
}
public string CreateAnswerMessage()
{
return $"{_yards} yards is {_meters} meters.";
}
}
public class OutputAnswer
{
public void WriteAnswer(string message)
{
Console.WriteLine(message);
}
}
ダウンロード
おわりに
SOLID原則は重要な原則ですので、本や他の web サイトなども参考にして学んでください。
次回は SOLID原則の「D」Dependency-inversion principle (依存性逆転の原則)に従ってリファクタリングします。
よろしければサポートをお願いします。 いただいたサポートは、書籍代、開発機器購入などに充てさせていただきます。