设计思路
将整个程序的业务逻辑分成以下几类:
- 核心逻辑:只考虑正确的数据,对数据进行变换,这里是纯函数的部分
- 异常处理:以异常来代表所有“不想理”的情况
- 非正确数据:对于数据进行范围判断,如果处于非法范围则抛出异常
- 其他异常:直接抛出即可
- 输入输出:
- 从
stdin
获取输入,转换到目标数据类型 - 从核心逻辑获取输出,输出到
stdout
- 从
- 基础设施 / 辅助类:需要辅助类将上述几个部分串起来,想到 javascript 中的
Promise
,它可以通过.then
链式的连接多个流水线上的数据处理函数,并且在最后可以衔接一个.catch
以统一处理异常,这种模式非常适合此设计需求。
核心逻辑
- 由
weight
和height
计算出BMI
- 设计出
float CalcBMI(float weight, float height)
- 考虑到需要在 Promise 中穿起来,都设计成
Func<T, R>
签名的函数更合适 - 因此改为
float CalcBMI((float weight, float height) arg)
- 设计出
- 由
BMI
得到对应的评价文本- 设计出
string BMIToText(float BMI)
- 设计出
static float CalcBMI((float weight, float height) arg) {
return arg.weight / (arg.height * arg.height);
}
static string BMIToText(float BMI) {
return $"BMI {BMI:F1} is " + (
BMI < 18.5f
? "underweight"
: BMI < 25f
? "normal weight"
: BMI < 30f
? "overweight"
: "obesity"
);
}
// 只读属性 Getter
static Func<(float weight, float height), string> GetBMIText
=> (arg) => BMIToText(CalcBMI(arg));
异常处理
- 需要对
weight
和height
作基本的范围限制- 设计出
bool IsBMIArgValid((float weight, float height) arg)
- 以及异常版
void RaiseIfBMIArgInvalid((float weight, float height) arg)
- 设计出
- 最后需要一个统一的异常处理入口
- 考虑到我们的程序很简单,遇到异常就提示并终止即可
- 因此这个统一的入口内部并不需要对异常进行分类诊断、作不同处理
- 设计出
void HandleException(Exception ex)
static bool IsBMIArgValid((float weight, float height) arg) {
return arg.weight > 0f
&& arg.height > 0f
&& arg.weight <= 200f
&& arg.height <= 2f;
}
static void RaiseIfBMIArgInvalid((float weight, float height) arg) {
if (!IsBMIArgValid(arg)) {
throw new ArgumentException(
$"weight({arg.weight} or height({arg.height}) is invalid. "
+ "Valid rules: 0 < weight <= 200, 0 < height <= 2."
);
}
}
static void HandleException(Exception ex) {
Console.WriteLine("Exception caught: " + ex.Message);
Console.WriteLine("Stack Trace:\n" + ex.StackTrace);
}
输入输出
- 从
stdin
获取输入,转换到需求的参数类型,获取或转换失败时抛出异常- 设计出
(float weight, float height) GetUserInput()
- 设计出
- 将 BMI 文本输出到控制台
- 使用现成的
Console.WriteLine
- 使用现成的
- 在程序结束时等待用户按下任意键以退出
- 设计出
void WaitForExit()
- 设计出
static (float weight, floatheight) GetUserInput() {
Console.Write("Enter weight and height separated by space: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) {
throw new ArgumentException("Input cannot be empty");
}
string[] parts = input.Trim().Split(' ');
if (parts.Length != 2) {
throw new ArgumentException(
"Input must contain two values separated by space"
);
}
float[] floats = parts.Select(float.Parse).ToArray();
return (floats[0], floats[1]);
}
static void WaitForExit() {
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
基础设施 / 辅助类
- 需要实现一个
Promise
来将业务函数串起来- 需要
Promise<R> Then<R>(Func<T, R> onFulfilled)
来连接核心逻辑 - 需要
static Promise<T> Begin(Func<T> produce)
来开始数据处理链 - 需要
Promise<T> End(Action<T> consume)
来终止数据处理链 - 需要
void Catch(Action<Exception> onRejected)
来统一处理过程中的异常
- 需要
public class Promise<T> {
private T? Value { get; }
private Exception? Ex { get; }
private Promise(T? value, Exception? ex) {
if (value == null && ex == null) {
throw new ArgumentException(
"Either value or exception must be non-null"
);
}
Value = value;
Ex = ex;
}
public static Promise<T> Resolve(T value) {
return new Promise<T>(value, null);
}
public static Promise<T> Reject(Exception ex) {
return new Promise<T>(default, ex);
}
public static Promise<T> Begin(Func<T> produce) {
return Promise<T>
.Resolve(default) // 任何参数无所谓,反正不消耗
.Then((_) => produce());
}
public Promise<T> End(Action<T> consume) {
return Then(
(value) => { consume(value); return value; }
);
}
public Promise<R> Then<R>(Func<T, R> onFulfilled) {
if (Ex != null || Value == null) {
return new Promise<R>(default, Ex);
}
try {
return new Promise<R>(onFulfilled(Value), null);
}
catch (Exception ex) {
return new Promise<R>(default, ex);
}
}
public void Catch(Action<Exception> onRejected) {
if (Value != null || Ex == null) {
return;
}
onRejected(Ex);
}
}
意识到 RaiseIfBMIArgInvalid
函数会消费掉数据而不继续传递,因此需要
- 设计装饰器函数将
Action<T>
包装为Func<T, T>
- 设计出
Func<T, T> PassWithAssert<T>(Action<T> assert)
- 设计出
public class FuncTools {
public static Func<T, T> PassWithAssert<T>(Action<T> assert) {
return (value) => {
assert(value);
return value;
};
}
}
优化
注意到 Promise
有 2 处实现有问题:
Begin
方法的.Resolve(default)
提示default
可能为null
- 这不是可能,而是一定,但直接写
null
又不行
- 这不是可能,而是一定,但直接写
End
方法的consume(value); return value;
会冗余的返回value
,这可能被意外使用
解决方案是设计一个 NoneType
类,它只有一个唯一的值 None
。
None
将表示这个值从不被消费也没有任何价值,它仅作为一个类型或值的占位符存在。
public class NoneType {
public static readonly NoneType None = new();
private NoneType() { } // 私有构造函数,确保只能有一个 None 实例
public override string ToString() {
return "None";
}
}
完整实现
Program.cs
using static bmi.FuncTools;
namespace bmi {
public class Program {
// 主函数
static void Main(string[] _) {
var passWithAssert = PassWithAssert<(float weight, float height)>
(RaiseIfBMIArgInvalid);
Promise<(float weight, float height)>
.Begin(GetUserInput)
.Then(passWithAssert)
.Then(GetBMIText)
.End(Console.WriteLine)
.Catch(HandleException);
WaitForExit();
}
// - 核心业务逻辑
static float CalcBMI((float weight, float height) arg) {
return arg.weight / (arg.height * arg.height);
}
static string BMIToText(float BMI) {
return $"BMI {BMI:F1} is " + (
BMI < 18.5f
? "underweight"
: BMI < 25f
? "normal weight"
: BMI < 30f
? "overweight"
: "obesity"
);
}
static Func<(float weight, float height), string> GetBMIText
=> (arg) => BMIToText(CalcBMI(arg));
// - 异常处理逻辑
static bool IsBMIArgValid((float weight, float height) arg) {
return arg.weight > 0f
&& arg.height > 0f
&& arg.weight <= 200f
&& arg.height <= 2f;
}
static void RaiseIfBMIArgInvalid((float weight, float height) arg) {
if (!IsBMIArgValid(arg)) {
throw new ArgumentException(
$"weight({arg.weight} or height({arg.height}) is invalid. "
+ "Valid rules: 0 < weight <= 200, 0 < height <= 2."
);
}
}
static void HandleException(Exception ex) {
Console.WriteLine("Exception caught: " + ex.Message);
Console.WriteLine("Stack Trace:\n" + ex.StackTrace);
}
// - 输入输出逻辑
static (float weight, float height) GetUserInput() {
Console.Write("Enter weight and height separated by space: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) {
throw new ArgumentException("Input cannot be empty");
}
string[] parts = input.Trim().Split(' ');
if (parts.Length != 2) {
throw new ArgumentException(
"Input must contain two values separated by space"
);
}
float[] floats = parts.Select(float.Parse).ToArray();
return (floats[0], floats[1]);
}
static void WaitForExit() {
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}
FuncTools.cs
namespace bmi {
public class FuncTools {
public static Func<T, T> PassWithAssert<T>(Action<T> assert) {
return (value) => {
assert(value);
return value;
};
}
}
}
Promise.cs
namespace bmi {
public class Promise<T> {
private T? Value { get; }
private Exception? Ex { get; }
private Promise(T? value, Exception? ex) {
if (value == null && ex == null) {
throw new ArgumentException(
"Either value or exception must be non-null"
);
}
Value = value;
Ex = ex;
}
public static Promise<T> Resolve(T value) {
return new Promise<T>(value, null);
}
public static Promise<T> Reject(Exception ex) {
return new Promise<T>(default, ex);
}
public static Promise<T> Begin(Func<T> produce) {
return Promise<NoneType>
.Resolve(NoneType.None) // 任何参数无所谓,反正不消耗
.Then((_) => produce());
}
public Promise<NoneType> End(Action<T> consume) {
return Then(
(value) => { consume(value); return NoneType.None; }
);
}
public Promise<R> Then<R>(Func<T, R> onFulfilled) {
if (Ex != null || Value == null) {
return new Promise<R>(default, Ex);
}
try {
return new Promise<R>(onFulfilled(Value), null);
}
catch (Exception ex) {
return new Promise<R>(default, ex);
}
}
public void Catch(Action<Exception> onRejected) {
if (Value != null || Ex == null) {
return;
}
onRejected(Ex);
}
}
}
NoneType.cs
namespace bmi {
public class NoneType {
public static readonly NoneType None = new();
private NoneType() { } // 私有构造函数,确保只能有一个 None 实例
public override string ToString() {
return "None";
}
}
}