Reputation: 27
I have 3 classes (1 absrtact(Services), and 2 derivatives). Plus, I have 2 different output formats (UA/EN). And I don't know how remake my code in order it follow a open closed principle. For example, if I want to add a German output format. I will need to edit each class.
using System;
using System.Globalization;
namespace naslidov
{
public abstract class Services
{
public string title;
public decimal price;
public Services(string title, decimal price)
{
this.title = title;
this.price = price;
}
public virtual string ToEnglish()
{
return $" ";
}
public virtual string ToUkraine()
{
return $"";
}
}
public class Food : Services
{
public DateTime expirationDate;
public Food(string title, decimal price, DateTime expirationDate)
: base(title, price)
{
this.title = title;
this.price = price;
this.expirationDate = expirationDate;
}
public override string ToEnglish()
{
return $"{base.title} |{price.ToString("N", CultureInfo.InvariantCulture)} uan | {expirationDate.ToString("d", CultureInfo.CreateSpecificCulture("ja-JP"))} |------ ";
}
public override string ToUkraine()
{
return $"{base.title} |{price.ToString("F", CultureInfo.InvariantCulture).Replace(".", ",")} uan | {expirationDate.ToString("dd.MM.yyyy")}| ------ ";
}
}
public class HouseholdAppliance : Services
{
public int warrantyPeriodInMonths;
public HouseholdAppliance(string title, decimal price, int warrantyPeriodInMonths)
: base(title, price)
{
this.title = title;
this.price = price;
this.warrantyPeriodInMonths = warrantyPeriodInMonths;
}
public override string ToEnglish()
{
return $"{base.title} |{price.ToString("N", CultureInfo.InvariantCulture)} uan | ------ |{warrantyPeriodInMonths} ";
}
public override string ToUkraine()
{
return $"{base.title} |{price.ToString("F", CultureInfo.InvariantCulture).Replace(".", ",")} uan | ------ |{warrantyPeriodInMonths} ";
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Enter region(UA/EN):");
string user_input = Console.ReadLine();
DateTime date1 = new DateTime(2002, 3, 25);
DateTime date2 = new DateTime(2022, 8, 17);
DateTime date3 = new DateTime(2005, 1, 10);
Services first = new Food("apple", 105324660120.58m, date1);
Services second = new Food("bananas", 3045.21m, date2);
Services third = new Food("nuts", 308540m, date3);
Services nofrst = new HouseholdAppliance("television", 25547.54m, 12);
Services noscd = new HouseholdAppliance("pilosos", 2756854m, 24);
Services nothir = new HouseholdAppliance("notebook", 32248, 36);
Services[] fullservices = new Services[] { first, second, third, nofrst, noscd, nothir };
Console.WriteLine("title | price | expirationDate | warrantyPeriodInMonths");
Console.WriteLine("-----------------------------------------------------------------------");
if (user_input == "EN")
{
foreach (Services fullservice in fullservices)
{
Console.WriteLine(fullservice.ToEnglish());
}
}
if (user_input == "UA")
{
foreach (Services fullservice in fullservices)
{
Console.WriteLine(fullservice.ToUkraine());
}
}
else if (user_input != "UA" && user_input != "EN")
{
Console.WriteLine(" Sorry, wrong input!");
}
}
}
}
Upvotes: 0
Views: 110
Reputation: 40160
First of all, I want to encourage to never refactor without a requirement or goal. If you are trying to refactor this code to make it extensible for something you don't know needs to be extended, you are not only likely to be wasting effort (YAGNI), but you may also end up with code that is harder to change in other ways you might need later.
Thus, for the purposes of this answer, I will assume that the requirement is to make this code extensible (open for extension). And that what you need to extend is the supported formats.
We will start by defining a new abstract class interface Formatter
IFormat
which will serve as extension point to add new format. Ideally, this IFormat
should not depend on any specific (concrete, not abstract) Services
, nor should Services
know about any specific IFormat
. That is, we want extending these to be as independent as possible.
Now, what do specific Services
need to format? I can see in the code that you need to know the format for dates and prices. So let us give methods to format those to IFormat
:
public interface IFormat
{
string FormatDate(DateTime date);
string FormatPrice(decimal price);
}
Add any other methods that make sense. I added the minimum for this case.
We can proceed to implement a formatter for English and one for Ukrane. Please excuse my naming convention.
public class FormatterEnglish : IFormat
{
public string FormatDate(DateTime date)
{
return date.ToString("d", CultureInfo.CreateSpecificCulture("ja-JP"));
}
public string FormatPrice(decimal price)
{
return price.ToString("N", CultureInfo.InvariantCulture);
}
}
public class FormatterUkrane : IFormat
{
public string FormatDate(DateTime date)
{
return date.ToString("dd.MM.yyyy");
}
public string FormatPrice(decimal price)
{
return price.ToString("F", CultureInfo.InvariantCulture).Replace(".", ",");
}
}
Now, let us rework Services
to use it. It will no longer have one method per format, but one single method that takes a IFormat
argument:
public abstract class Services
{
public decimal price;
public string title;
public Services(string title, decimal price)
{
this.title = title;
this.price = price;
}
public abstract string ToString(IFormat formatter);
}
And, of course, we need to implement it in HouseholdAppliance
:
public override string ToString(IFormat formatter)
{
return $"{base.title} |{formatter.FormatPrice(price)} uan | ------ |{warrantyPeriodInMonths} ";
}
And Food
:
public override string ToString(IFormat formatter)
{
return $"{base.title} |{formatter.FormatPrice(price)} uan | {formatter.FormatDate(expirationDate)} |------ ";
}
To choose our IFormat
, I suggest a factory method. For example:
private static IFormat? CreateFormatter(string formatName)
{
if (formatName == "EN")
{
return new FormatterEnglish();
}
if (formatName == "UA")
{
return new FormatterUkrane();
}
return null;
}
You may also be interested in using type discovery, and specifying the format name in a custom attribute. That is beyond the scope of this answer.
Finally, you can use it like this:
var formatter = CreateFormatter(user_input);
if (formatter == null)
{
Console.WriteLine(" Sorry, wrong input!");
return;
}
foreach (Services fullservice in fullservices)
{
Console.WriteLine(fullservice.ToString(formatter));
}
Having a second look at the code, it is possible to extract the format template from the Services
. The IFormat
would need the template and the data. A solution like FormatWith
would make that easier. Anyway, I believe this answer addresses the question as it is.
Upvotes: 3