我写了一个年龄计算器,将birthDate作为输入。

我希望对此做一个一般的回顾。我特别关心message变量和try/catch语句后的行。

namespace Age
{
    class Program
    {
        static void Main(string[] args)
        {
            while (true)
            {
                try
                {
                    Console.Write("Enter your birtdate: ");
                    DateTime birthDate = DateTime.Parse(Console.ReadLine());

                    int Days = (DateTime.Now.Year * 365 + DateTime.Now.DayOfYear) - (birthDate.Year * 365 + birthDate.DayOfYear);
                    int Years = Days / 365;
                    string message = (Days >= 365) ? "Your age: " + Years + " years" : "Your age: " + Days + " days";

                    Console.WriteLine(message);
                }
                catch
                {
                    Console.WriteLine("You have entered an invalid date.\n");
                }

                Console.WriteLine("Exit? (y/n)");
                string userValue = Console.ReadLine();

                if (userValue == "y")
                {
                    Environment.Exit(0);
                }
            }
        }
    }
}


评论

您是否需要特定的反馈信息,或者只希望进行一般审查?

应该将其标记为作业吗? contain年包含366天。

我只想进行一般审查。不过,对于“字符串消息”变量和try-catch语句后的行,我仍然不确定。我以前从未真正终止过这样的应用程序。这不是家庭作业,只是我在业余爱好上编程。

#1 楼



您应该给用户输入生日时要遵循的格式

Console.Write("Enter your birthdate (MM/DD/YYYY): ");


您应该给用户一种关闭程序的方式。尽管这对于简单的学习程序而言并不是十分必要,但要养成良好的习惯。

您应避免引发异常,因为它们会破坏程序的流程。当然,在必要的地方使用它们,但这不是这些地方之一。在您的函数无法处理情况/错误的情况下,应使用异常。

在以下代码段中,我声明了DateTime对象和一个布尔值,它将告诉我们如果用户输入的字符串能够被解析。注意我如何调用TryParse方法。 TryParse方法采用一个字符串和一个out DateTime object。这很重要,out关键字比您遇到的要先进一些。这是一个关键字,它实现程序员使用C指针使用的功能。可以说此函数会将您传入的任何对象都设置为已解析的值。

DateTime birthDate;
bool succeeded = DateTime.TryParse(Console.ReadLine(), out birthDate);


因为TryParse方法返回布尔值,所以您只能在if语句中使用它,该条件取决于TryParse返回true。

if(DateTime.TryParse(Console.ReadLine(), out birthDate))
{
    // more stuff
}
else
    Console.WriteLine("You have entered an invalid date.");



我注意到您在程序中使用了\n。虽然我会说我一直都在犯这个罪,但实际上应该使用Environment.NewLine。原因是由于换行符/回车符在不同程序中的返回不一致。 Environment.NewLine始终使用正确的行尾。

Console.WriteLine("You have entered an invalid date." + Environment.NewLine);



您可以在DateTime对象上使用+或-运算符,从而产生TimeSpan对象

TimeSpan age = DateTime.Now - birthDate;


TimeSpan对象具有“天数”,“小时数”等属性。计算完成后,确实考虑了leap年。


通过发送要插入到字符串中的参数,可以更轻松地打印到控制台。

Console.WriteLine("Your age: {0} years and {1} days", (int)(age.Days/365.25), age.Days % 365.25);





总的来说,您的程序看起来像像这样。注意:我没有以编程方式解决用户转义程序的问题,我注意到您使用该程序更新了OP。

while (true)
{
    Console.Write("Enter your birtdate (MM/DD/YYYY): ");
    DateTime birthDate;
    if (DateTime.TryParse(Console.ReadLine(), out birthDate))
    {
        TimeSpan age = DateTime.Now - birthDate;
        Console.WriteLine("Your age: {0} years and {1} days", (int)(age.Days/365.25), age.Days % 365.25);
    }
    else
        Console.WriteLine("You have entered an invalid date." + Environment.NewLine);
}



编辑:当然是个人喜好,但我不喜欢(我认为这是不必要的括号,用来代替换行符)。

else
{
    Console.WriteLine("You have entered an invalid date." + Environment.NewLine);
}


有些人喜欢,我不是,这取决于每个编码人员。如果要在方括号周围加上括号,我可能会这样做

else
    { Console.WriteLine("You have entered an invalid date." + Environment.NewLine); }


评论


\ $ \ begingroup \ $
我认为,比NewLine更好的是为第二个换行符使用单独的第二个Console.WriteLine()。
\ $ \ endgroup \ $
– svick
2014年3月17日22:47

\ $ \ begingroup \ $
另外,我认为在这里使用365.25并不合理。例如,700%365.25为334.75,我认为这不是您想要的。
\ $ \ endgroup \ $
– svick
2014年3月17日22:50

\ $ \ begingroup \ $
3.您的if-else语句对我来说真的很奇怪,因为if部分使用方括号,而else部分未使用方括号。
\ $ \ endgroup \ $
–马克·安德烈(Marc-Andre)
2014年3月18日在16:48

\ $ \ begingroup \ $
@svick我几乎建议过,但是我希望他注意Environment.Newline。此外,365.25只是用于计算leap年的。
\ $ \ endgroup \ $
– BenVlodgi
2014年3月18日在16:52

\ $ \ begingroup \ $
@ Marc-Andre可以将自己的感受括起来,尽管它们不匹配,但我认为当用括号包围时,单行看起来很难看。如果您愿意,甚至可以在行的开头和结尾处放置方括号。
\ $ \ endgroup \ $
– BenVlodgi
2014年3月18日在16:53

#2 楼

我本来打算编辑其他答案,但是我将采取另一种方法-我的意思是,我将其推到了极点;)


警告
我将其推到了极致,这是从字面上看。该解决方案的目的不是解决简单的年龄计算器问题,而是说明如何构建SOLID应用程序-如果目标只是计算两个日期之间的差,那绝对是过大了。如果目标是学习如何使用琐碎/简单的问题作为借口来编写好的OOP ...
,请搭便车。


静态void Main(string [] args)
到现在为止,这是应用程序的入口点。当此static方法返回时,程序结束。要终止程序,只需在此方法中使用return;或构建程序流,以便正常退出仅会使主线程(想象一个游标按顺序运行每条指令-或在调试器中逐步执行代码)到达底部。 Main方法。
此方法为static,如果要调用其外部的任何内容,则也必须为static。如果您要编写过程代码,那就没关系。
如果您要使用面向对象的代码,则Main方法可能具有非常高的抽象水平,并且与伪代码一样读,如果不是这样的话简单的英语。
方法围绕着一个概念,即程序的诞生,生死。
依赖注入(DI)的门徒(有罪!)称此入口点为构成根。在这里实例化应用程序(及其依赖项),然后运行它。
简化到极致:
static void Main(string[] args)
{
    var app = new MyApplication();
    app.Run();
}

Run()方法有什么?
public class MyApplication
{
    public MyApplication()
    {
        // initialisation here
    }

    public void Run()
    {
        // app logic here
    }
}

请注意,Run方法不是static。它仅作为此MyApplication类定义的对象的接口的成员存在-换句话说,您需要MyApplication的实例才能调用此方法。
在这种情况下,应用程序逻辑部分将以我们的主循环为特征,根据条件从该循环退出。
public void Run()
{
    var keepRunning = true;

    while(keepRunning)
    {
        // application logic
        keepRunning = false; // exit
    }
}

至此,我们已经到达了无可挽回的地步。我们用Run方法编写的任何其他内容都会影响可维护性,可测试性和可读性。最好将其最小化。

关键在于委派工作。如果不使用MyApplication关键字,则new类无法单独完成其工作,这会增加耦合,而不能单独完成所有工作,这会降低内聚性。由于我们需要低耦合和高内聚性,因此我们首先避免使用static方法和new关键字。

为什么?
好的代码是可测试的代码。您将希望能够为所编写的代码编写测试-不管是否编写这些测试,编写可测试的代码都倾向于生成更具凝聚力和耦合性的代码。
另请参见:https:/ /stackoverflow.com/a/3085419/1188513

IUserInteraction
我们知道我们想使用控制台与用户进行交互,但是为了测试我们的应用程序逻辑,我们需要以便能够用我们的测试来替代用户的输入。
public interface IUserInputProvider
{
    string GetUserInput(string prompt);
    T GetUserInput<T>(string prompt, IUserInputValidator<T> validator);
}

-仅此而已,我们已经有足够的时间回到MyApplication类:
public class MyApplication
{
    private readonly IUserInputProvider _inputProvider;

    public MyApplication(IUserInputProvider inputProvider)
    {
        _inputProvider = inputProvider;
    }

    public void Run()
    {
        var keepRunning = true;

        while(keepRunning)
        {
            var prompt = "Enter your birth date:";
            var input = _inputProvider.GetUserInput(prompt); // no validation for now
            // ...

            keepRunning = false; // exit
        }
    }
}

稍后我将返回IUserInputValidator<T>

模拟
GetUserInput是一种返回string的方法。仅此而已。我们知道我们想调用Console.ReadLine(),但这是MyApplication类不需要知道的实现细节。
如果我们要编写测试来查看当用户输入“ y”时Run方法是否有效退出”,我们不会打开控制台并等待有人输入“ y”-而是会建立一个模拟-IUserInputProvider接口的“伪”实现,当我们向GetUserInput询问时返回“ y”。 />
实施
我们将要使用的具体实现将使用控制台。没有什么可以阻止做出另一个弹出对话框的具体实现的-只要我们使用prompt并返回string,任何东西都可以工作。这意味着可以以各种可能的方式修改实现,MyApplication类所做的唯一假设是存在一个GetUserInput方法,该方法采用string prompt并返回string
这可能是一个实现:
public class ConsoleUserInputProvider : IUserInputProvider
{
    public string GetUserInput(string prompt)
    {
        Console.WriteLine(prompt);
        return Console.ReadLine();
    }

    public T GetUserInput<T>(string prompt, IUserInputValidator<T> validator)
    {
        string input;
        T result;

        var isValidInput = false;
        while(!isValidInput)
        {
            input = GetUserInput(prompt);
            isValidInput = validator.Validate(input, out result);
        }

        return result;
    }
}

IUserInputValidator是另一个公开bool Validate(string input)方法的抽象。
让它成为通用接口:
public interface IUserInputValidator<T>
{
    bool Validate(string input, out T result);
}

一个人可以这样实现:
public class BirthDateValidator : IUserInputValidator<DateTime>
{
    public bool Validate(string input, out DateTime result)
    {
        return DateTime.TryParse(input, out result);
    }
}

或者像这样:
public enum YesNoResult
{
    Unknown,
    Yes,
    No
}

public class YesNoValidator : IUserInputValidator<YesNoResult>
{
    private readonly IDictionary<string, YesNoResult> _values;

    public YesNoValidator(IDictionary<string, YesNoResult> values)
    {
        _values = values;
    }

    public bool Validate(string input, out YesNoResult result)
    {
        if (string.IsNullOrEmpty(input))
        {
            throw new ArgumentException("input", "input string is empty.");
        }

        var lowerCase = input.Substring(0, 1).ToLower();
        
        var isValue = values.TryGetValue(lowerCase, out result);
        if (!isValue)
        {
            result = YesNoResult.Unknown;
        }
        
        return (result != YesNoResult.Unknown);
    }
}


如您所见,这种方法产生了非常集中和专门的代码-代码做得很好,以至于不可能甚至需要改变。但是它仍然可以扩展-您可以使用可以记录所有失败验证的Validator装饰任何ValidationLoggerDecorator实现:
public class ValidationLoggerDecorator<T> : IUserInputValidator<T>
{
    private readonly IUserInputValidator<T> _validator;
    private readonly ILogger _logger;

    public ValidationLoggerDecorator(IUserInputValidator<T> validator, ILogger logger)
    {
        _validator = validator;
        _logger = logger;
    }

    public bool Validate(string input, out T result)
    {
        var isValid = _validator.Validate(input, out result);

        if (!isValid)
        {
            _logger.Info(string.Format("Validation failed for input '{0}'.", input));
        }

        return isValid;
    }
}

让我们退后一步,看看我们在这里拥有什么:


代码始终遵循单一职责原则-遵循单一职责原则。


装饰器示例中描述的可扩展性是侧面的-打开/关闭原则的效果:关闭类以进行修改,打开以进行扩展。


MyApplication类可以与IUserInputProvider接口的任何实现一起使用的事实实现可能具有的依赖关系是Liskov替换原理的副作用。


遵循接口隔离原理也使我们的接口非常集中,理想情况下只公开一个成员。这一点极大地影响了凝聚力。


所有实现(“具体”类)都依赖于抽象,并且这些依赖项已注入到其构造函数中,这一事实遵循了“依赖关系反转原理” *。


这5点合在一起拼写为SOLID。

我们传递给IUserInputValidator方法的GetUserInput()必须来自某个地方。但是,如果我们创建一个new BirthDateValidator(),则我们的类将与该特定实现紧密结合,并且将很难从外部测试GetUserInput()方法和控件验证。
MyApplication类可以在其内部接收所需的验证器。构造函数,我们可以从Run方法中提取一些逻辑到自己的私有方法中:
public class MyApplication
{
    private readonly IUserInputProvider _inputProvider;
    private readonly IUserInputValidator<DateTime> _dateValidator;
    private readonly IUserInputValidator<YesNoResult> _confirmationValidator;

    public MyApplication(IUserInputProvider inputProvider, 
                         IUserInputValidator<DateTime> dateValidator, 
                         IUserInputValidator<YesNoResult> confirmationValidator)
    {
        _inputProvider = inputProvider;
        _dateValidator = dateValidator;
        _confirmationValidator = confirmationValidator;
    }

    public void Run()
    {
        var keepRunning = true;

        while(keepRunning)
        {
            var date = GetBirthDate();
            // ...

            keepRunning = !GetExitConfirmation();
        }
    }

    private DateTime GetBirthDate()
    {
        var prompt = "Enter your birth date:";
        var input = _inputProvider.GetUserInput(prompt, _dateValidator);
        
        return DateTime.Parse(input);
    }

    private bool GetExitConfirmation()
    {
        var prompt = "Exit (Y|N)?";
        var input = _inputProvider.GetUserInput(confirmPrompt, _confirmationValidator);

        return input == YesNoResult.Yes;
    }
}

请注意,构造函数如何很容易因可能要使用的验证程序数量过多而肿从用户那里得到。 3个构造函数参数可能没问题。但是,不仅如此,我很想将验证器提取到自己的对象中,以使消息保持清晰:MyApplication类需要验证器-它不进行验证。
无论如何都要提取它们以查看我们得到的结果是:
public class UserInputValidation
{
    private readonly IUserInputValidator<DateTime> _dateValidator;
    private readonly IUserInputValidator<YesNoResult> _confirmationValidator;

    public UserInputValidation(IUserInputValidator<DateTime> dateValidator, 
                               IUserInputValidator<YesNoResult> confirmationValidator)
    {
        _dateValidator = dateValidator;
        _confirmationValidator = confirmationValidator
    }

    public IUserInputValidator<DateTime> DateValidator { get { return _dateValidator; } }
    public IUserInputValidator<YesNoResult> ConfirmationValidator { get { return _confirmationValidator; } }
}


因为实际的计算算法本身就是更改的原因,所以最好将其封装在自己的类中。
@svick的答案可能是某些IAgeCalculator接口的实现。
现在MyApplication类如下所示:
public class MyApplication
{
    private readonly IUserInputProvider _inputProvider;
    private readonly IAgeCalculator _calculator;
    private readonly UserInputValidation _validation;

    public MyApplication(IUserInputProvider inputProvider,
                         IAgeCalculator calculator, 
                         UserInputValidation validation)
    {
        _inputProvider = inputProvider;
        _calculator = calculator;
        _validation = validation;
    }

    public void Run()
    {
        var keepRunning = true;

        while(keepRunning)
        {
            var date = GetBirthDate();
            DisplayAge(date);

            keepRunning = !GetExitConfirmation();
        }
    }

    private DateTime GetBirthDate()
    {
        var prompt = "Enter your birth date:"; 
        var input = _inputProvider.GetUserInput(prompt, _validation.DateValidator);
        
        return DateTime.Parse(input);
    }

    private void DisplayAge(DateTime date)
    {
        var result = _calculator.GetDifferenceInYearsAndDays(date, DateTime.Today);
        var message = string.Format("Your age: {0} years and {1} days", result.Item1, result.Item2);
        _inputProvider.ShowMessage(message);
    }

    private bool GetExitConfirmation()
    {
        var prompt = "Exit (Y|N)?"; 
        var input = _inputProvider.GetUserInput(confirmPrompt, _validation.ConfirmationValidator);

        return input == YesNoResult.Yes;
    }
}


以上假设ShowMessage接口中添加了IUserInputProvider方法;该方法的实现方式如下:
public void ShowMessage(string message)
{
    Console.WriteLine(message);
}


现在,Main方法可以满足其目的:编写应用程序!
static void Main(string[] args)
{
    var input = new ConsoleInputProvider();
    var calculator = new AgeCalculator();
    var logger = LogManager.GetLogger("logger"); // gets a NLog logger
    var dateValidator = new ValidationLoggerDecorator(new BirthDateValidator(), logger);
    var yesNoValues = new Dictionary<string, YesNoResult>
                          {
                              { "y", YesNoResult.Yes },
                              { "n", YesNoResult.No }
                          };
    var confirmationValidator = new YesNoValidator(values);
    var validation = new UserInputValidation(dateValidator, confirmationValidator);

    var app = new MyApplication(input, calculator, validation);
    app.Run();
}

如您所见,控制反转使我们可以轻松地改变主意并将dateValidator换成简单的BirthDateValidator,或编写一个单元测试,它将仅测试日期验证的运行方式,或仅计算年龄的计算方式,与其他所有因素无关
当然,这个琐碎的应用程序的合成根目录是手动实例化对象(又名穷人的DI),并且非常易于管理。对于更大的应用程序,您可以将此艰巨的任务留给您最喜欢的IoC容器,让您的Main方法看起来像这样(此处使用Ninject):
static void Main(string[] args)
{
    var kernel = new StandardKernel(new MyApplicationNinjectModule());
    var app = kernel.Get<MyApplication>();
    app.Run();
}

整个应用程序的依赖关系图可以在一个单独的图中解析方法调用-Ninject并不是最快的方法,但是其漂亮的语法和出色的可扩展性使其成为考虑的可靠选择。如果您不知道StandardKernel的功能,那么如果我告诉您它与前一片段完全相同,则上面的代码似乎是自动的。
这足够长了。

评论


\ $ \ begingroup \ $
“理想情况下仅公开一个成员”是否意味着所有内容都可以声明为委托而不是接口?例如,不是IUserInputProvider inputProvider,而是Func GetUserInput(输入参数是提示,返回值是用户输入)?而不是IUserInputValidator ConfirmationValidator,Func 验证吗?对于接口,您需要声明接口,在接口中声明方法,声明实现接口的单独类,构造该类的实例,...
\ $ \ endgroup \ $
– ChristW
2014年3月18日在13:34

\ $ \ begingroup \ $
...对于委托,您只需声明委托类型,或使用Func <>重载之一来声明委托类型;实现委托的方法可以是任何东西,甚至可以是Main或Application或Test类的静态方法。
\ $ \ endgroup \ $
– ChristW
2014年3月18日在13:36

\ $ \ begingroup \ $
也许使用接口而不是委托来教授/实现这一点是对Java的困扰?
\ $ \ endgroup \ $
– ChristW
2014年3月18日在13:42

\ $ \ begingroup \ $
@ChrisW大声笑,这在许多方面都是过分的;确实,代表会更简单-想法是将OP的代码/项目用作“小型应用程序”,以展示人们将如何使用这些技术制作“大型应用程序”(如非常小型的应用程序是否需要解耦? )。 OTOH我确实倾向于未充分利用委托和过度使用接口,您有一点要说:)
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2014年3月18日在13:48



\ $ \ begingroup \ $
我倾向于使用接口(或类)而不是委托(或方法),只要有两种相关/相互依赖的方法,而不仅仅是一种。
\ $ \ endgroup \ $
– ChristW
2014年3月18日14:17

#3 楼


Console.Write("Enter your birtdate: ");
DateTime birthDate = DateTime.Parse(Console.ReadLine());



您的catch块看起来像是假设ParseException会被上述DateTime.Parse调用抛出。因此,最好对此进行明确说明:

catch(ParseException)
{
    Console.WriteLine("You have entered an invalid date.\n");
}


这样,如果在循环中的其他地方抛出了另一个异常类型,则不会显示误导性消息。

现在,如何使用DateTime.ParseExact,并指定预期的日期格式?

Console.Write("Enter your birtdate (yyyy-MM-dd): ");
DateTime birthDate = DateTime.ParseExact(Console.ReadLine(), "yyyy-MM-dd", CultureInfo.InvariantCulture);


那太好了,单行代码中发生了太多事情:



Controle.ReadLine()正在获取控制台输入

DateTime.Parse | DateTime.ParseExact正在解析控制台输入

您应该将其分解为2个单独的指令:

Console.Write("Enter your birtdate (yyyy-MM-dd): ");
var input = Console.ReadLine();
var birthDate = DateTime.ParseExact(input, "yyyy-MM-dd", CultureInfo.InvariantCulture);


请注意,我使用var是为了简洁在这里,inputstringbirthDateDateTime。下一步是从这几行中提取一个方法-有一个名为GetBirthDate的方法,该方法返回DateTime或抛出一个ParseException您的catch块可以处理。

#4 楼

为了正确计算日期差,您需要考虑leap年。并且您需要使用适当的规则来确定何时是a年。幸运的是,.Net库已经知道了这一切。

那么,怎么做呢?找出该人的上一个生日,然后计算自出生日期起的年差,然后计算直到今天的天差。在代码中:

public static Tuple<int, int> GetDifferenceInYearsAndDays(DateTime birth, DateTime today)
{
    if (birth > today)
        throw new InvalidOperationException();

    var thisYearBirthday = new DateTime(today.Year, birth.Month, birth.Day);
    var lastBirthday =
        thisYearBirthday <= today ? thisYearBirthday : thisYearBirthday.AddYears(-1);

    int years = lastBirthday.Year - birth.Year;
    int days = (today - lastBirthday).Days;

    return Tuple.Create(years, days);
}


这可能比您的代码更复杂,但这是正确的,而且我也认为它更具可读性。

该代码位于单独的方法中,以遵循关注点的分离。


还有一件事:每当使用DateTime.Now(或DateTime.Today)时,您应该只访问该属性一次并存储该值在变量之类的东西中。否则,当其他所有人庆祝新年时,您的代码可能会有一个非常奇怪的错误。

评论


\ $ \ begingroup \ $
我刚刚意识到,如果生日是2月29日,这将行不通。在保持代码相对简单的同时,不确定如何解决该问题。
\ $ \ endgroup \ $
– svick
2014年3月18日在11:52

\ $ \ begingroup \ $
假设in年的生日是2月28日:thisYearBirthday = new DateTime(today.Year,birth.Month,birth.Day> DateTime.DaysInMonth(today.year,birth.Month)?DateTime.DaysInMonth (今天,年份,出生月份):出生日期。但是为DaysInMonth引入变量会更好。也许这会使代码不再那么简单了
\ $ \ endgroup \ $
–致谢
2014年3月21日在12:36