在 Blazor 应用程序中,关注点分离和单一责任原则的最佳实践是什么?
In a Blazor app, what is best practice for separation of concerns and single responsibility principles?
我正在使用从派生自 ComponentBase 的基础继承的 Razor 文件。我的理解是,这两个文件一般应该负责处理UI相关的任务。话虽这么说,我应该在低级组件中调用我的数据服务吗?或者我应该将对它们的调用保留在可以编排数据服务的更高级别的组件中,然后简单地将数据传递给组件以处理渲染? (当我提到高或低级别时,我的意思是父组件将是高级别而孙组件将是低级别)
据我了解,为处理数据服务而注入的接口将拥有相同的资源(作为单例)。所以我的问题与其说是关于资源管理,不如说是关于保持稳定。应该在哪里使用数据服务?无处不在还是孤立无援?谢谢!
我将大力支持将服务隔离到一个基地 class。在我得出这个结论之前,我一直 运行 关注的问题是,随着应用程序大小和复杂性的增加,将服务调用分散到各处会让人感到困惑。将每个组件构建为一个原子的东西,它自己处理所有事情并注入它的服务是非常诱人的,但是一旦所有这些组件开始组合在一起并需要开始相互交谈,它就会变得非常令人头疼。当你有类似单例的东西时,这会复杂化,其中可能涉及任何状态,因为组件的基础状态可以很容易地被另一个组件更改。 (有时不是故意的 - 请参阅 EF Core 和数据跟踪以及当从 2 个组件引用被跟踪的数据时你可以获得的乐趣 - 或者更糟的是,Blazor 服务器上的 2 个单独的客户端连接)在你知道之前,有太多地方寻找错误或在需要进行更改时进行更改,而跟踪错误变得噩梦般。
组件自治的第二条途径是使用级联参数,但无论何时你这样做,你都会将组件耦合到 DOM 树上某处的具体组件,避免耦合是 SOLID 的重点.通常最好让每个组件都代表非常简单的功能,这些功能可以组合起来为用户创造更丰富的体验。
所以我发现成功的地方是像你在基础 class 中提到的那样隔离服务,然后让 DOM 树下的每个组件尽可能地愚蠢,这有一个对我的输出以及发现和修复错误的能力产生了巨大影响。事实上,在我开始使用这种方法之前,我有一个项目不得不放弃两次,现在我正在处理一个功能性应用程序,并以良好的方式构建功能。 (谢天谢地,这是一个业余爱好项目!)
这个方法一点也不复杂。在基础 class 中,我将在需要时将方法调用和属性公开为受保护的,并尽可能将其他所有内容保密,因此外部可见性处于绝对最低限度。所有服务调用也发生在基 class 中,并封装在私有方法中,这会中断服务与 UI 之间的连接。然后,我会将数据作为组件参数向下传递到 DOM 树中,并将功能作为 EventCallback<T>
类型的参数向下传递。
以 classic 订单列表为例。我可以按客户 ID 加载订单列表,然后通过使用表达式主体成员过滤主列表来公开打开的订单列表和关闭的订单列表。所有这些都发生在基础 class 中,但我将其设置为 UI 唯一可以访问的是子列表和方法。在下面的示例中,我通过控制台日志表示服务调用,但您会明白这个想法,以及您在问题中提到构建内容的方式本质上是这样的:
OrdersBase.cs
public class OrdersBase : ComponentBase
{
private List<Order> _orders = new List<Order>();
protected List<Order> OpenOrders => _orders.Where(o => o.IsClosed == false).ToList();
protected List<Order> ClosedOrders => _orders.Where(o => o.IsClosed == true).ToList();
protected void CloseOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = true;
Console.WriteLine($"Service was called to close order #{order.Id}");
}
protected void OpenOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = false;
Console.WriteLine($"Service was called to open order #{order.Id}");
}
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Calling service to fill the orders list for customer #1...");
// quick mock up for a few orders
_orders = new List<Order>()
{
new Order() { Id = 1, OrderName = "Order Number 1", CustomerId = 1 },
new Order() { Id = 2, OrderName = "Order Number 2", CustomerId = 1 },
new Order() { Id = 3, OrderName = "Order Number 3", CustomerId = 1 },
new Order() { Id = 4, OrderName = "Order Number 4", CustomerId = 1 },
new Order() { Id = 5, OrderName = "Order Number 5", CustomerId = 1 },
};
Console.WriteLine("Order list filled");
}
}
现在我可以使用顶级组件中的基本 class,并且我将只能访问受保护和 public 成员。我可以使用这个高级组件来协调 UI 的排列方式并为委托传递方法,这就是它要做的全部。结果这很轻。
Orders.razor
@page "/orders"
@inherits OrdersBase
<div>
<h3>Open Orders:</h3>
<OrdersList Orders="OpenOrders" OnOrderClicked="CloseOrder" />
</div>
<div>
<h3>Closed Orders:</h3>
<OrdersList Orders="ClosedOrders" OnOrderClicked="OpenOrder" />
</div>
然后 OrderList 组件负责呈现 OrderItems 列表并传递委托操作。同样,只是一个简单的、愚蠢的组件。
OrderList.razor
<div>
@foreach (var order in Orders)
{
<OrderItem Order="order" OnOrderClicked="OnOrderClicked.InvokeAsync" />
}
</div>
@code {
[Parameter]
public List<Order> Orders { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
}
现在 OrderItem 列表可以呈现有关订单的内容并充当点击目标,当订单被点击时,它会一直调用委托返回到基础 class,这就是那里该方法运行。 OrderClicked 方法还检查 EventCallback,因此如果没有分配委托,则单击不会执行任何操作。
OrderItem.razor
<div @onclick="OrderClicked">
<p>Order Name: @Order.OrderName</p>
</div>
@code {
[Parameter]
public Order Order { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
private void OrderClicked()
{
if(OnOrderClicked.HasDelegate)
{
OnOrderClicked.InvokeAsync(Order);
}
}
}
所有这些组合在一起构成了显示订单的组件,如果您单击打开的订单,它会移动关闭的订单列表,反之亦然。所有的逻辑都在基础 class 中,每个组件都有一个简单的工作要做,这使得推理变得容易得多。
这也会让我知道何时需要将一个组件分解成更小的组件。我的理念是不要一次向用户展示太多内容,因此每个页面都应该简洁、简单,并且不要期望做太多事情。为此,当我构建这样的东西时,当我的基础 class 或父 UI razor 文件开始膨胀时,我可以说我会走得很远,这会提示重构部分另一个专用页面的功能。它可以生成更多文件,但也使构建和维护变得更加容易。
这是对一个简短问题的长回答。你可能同意我的看法,也可能不同意,但希望它能帮助你决定如何继续进行。
我正在使用从派生自 ComponentBase 的基础继承的 Razor 文件。我的理解是,这两个文件一般应该负责处理UI相关的任务。话虽这么说,我应该在低级组件中调用我的数据服务吗?或者我应该将对它们的调用保留在可以编排数据服务的更高级别的组件中,然后简单地将数据传递给组件以处理渲染? (当我提到高或低级别时,我的意思是父组件将是高级别而孙组件将是低级别)
据我了解,为处理数据服务而注入的接口将拥有相同的资源(作为单例)。所以我的问题与其说是关于资源管理,不如说是关于保持稳定。应该在哪里使用数据服务?无处不在还是孤立无援?谢谢!
我将大力支持将服务隔离到一个基地 class。在我得出这个结论之前,我一直 运行 关注的问题是,随着应用程序大小和复杂性的增加,将服务调用分散到各处会让人感到困惑。将每个组件构建为一个原子的东西,它自己处理所有事情并注入它的服务是非常诱人的,但是一旦所有这些组件开始组合在一起并需要开始相互交谈,它就会变得非常令人头疼。当你有类似单例的东西时,这会复杂化,其中可能涉及任何状态,因为组件的基础状态可以很容易地被另一个组件更改。 (有时不是故意的 - 请参阅 EF Core 和数据跟踪以及当从 2 个组件引用被跟踪的数据时你可以获得的乐趣 - 或者更糟的是,Blazor 服务器上的 2 个单独的客户端连接)在你知道之前,有太多地方寻找错误或在需要进行更改时进行更改,而跟踪错误变得噩梦般。
组件自治的第二条途径是使用级联参数,但无论何时你这样做,你都会将组件耦合到 DOM 树上某处的具体组件,避免耦合是 SOLID 的重点.通常最好让每个组件都代表非常简单的功能,这些功能可以组合起来为用户创造更丰富的体验。
所以我发现成功的地方是像你在基础 class 中提到的那样隔离服务,然后让 DOM 树下的每个组件尽可能地愚蠢,这有一个对我的输出以及发现和修复错误的能力产生了巨大影响。事实上,在我开始使用这种方法之前,我有一个项目不得不放弃两次,现在我正在处理一个功能性应用程序,并以良好的方式构建功能。 (谢天谢地,这是一个业余爱好项目!)
这个方法一点也不复杂。在基础 class 中,我将在需要时将方法调用和属性公开为受保护的,并尽可能将其他所有内容保密,因此外部可见性处于绝对最低限度。所有服务调用也发生在基 class 中,并封装在私有方法中,这会中断服务与 UI 之间的连接。然后,我会将数据作为组件参数向下传递到 DOM 树中,并将功能作为 EventCallback<T>
类型的参数向下传递。
以 classic 订单列表为例。我可以按客户 ID 加载订单列表,然后通过使用表达式主体成员过滤主列表来公开打开的订单列表和关闭的订单列表。所有这些都发生在基础 class 中,但我将其设置为 UI 唯一可以访问的是子列表和方法。在下面的示例中,我通过控制台日志表示服务调用,但您会明白这个想法,以及您在问题中提到构建内容的方式本质上是这样的:
OrdersBase.cs
public class OrdersBase : ComponentBase
{
private List<Order> _orders = new List<Order>();
protected List<Order> OpenOrders => _orders.Where(o => o.IsClosed == false).ToList();
protected List<Order> ClosedOrders => _orders.Where(o => o.IsClosed == true).ToList();
protected void CloseOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = true;
Console.WriteLine($"Service was called to close order #{order.Id}");
}
protected void OpenOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = false;
Console.WriteLine($"Service was called to open order #{order.Id}");
}
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Calling service to fill the orders list for customer #1...");
// quick mock up for a few orders
_orders = new List<Order>()
{
new Order() { Id = 1, OrderName = "Order Number 1", CustomerId = 1 },
new Order() { Id = 2, OrderName = "Order Number 2", CustomerId = 1 },
new Order() { Id = 3, OrderName = "Order Number 3", CustomerId = 1 },
new Order() { Id = 4, OrderName = "Order Number 4", CustomerId = 1 },
new Order() { Id = 5, OrderName = "Order Number 5", CustomerId = 1 },
};
Console.WriteLine("Order list filled");
}
}
现在我可以使用顶级组件中的基本 class,并且我将只能访问受保护和 public 成员。我可以使用这个高级组件来协调 UI 的排列方式并为委托传递方法,这就是它要做的全部。结果这很轻。
Orders.razor
@page "/orders"
@inherits OrdersBase
<div>
<h3>Open Orders:</h3>
<OrdersList Orders="OpenOrders" OnOrderClicked="CloseOrder" />
</div>
<div>
<h3>Closed Orders:</h3>
<OrdersList Orders="ClosedOrders" OnOrderClicked="OpenOrder" />
</div>
然后 OrderList 组件负责呈现 OrderItems 列表并传递委托操作。同样,只是一个简单的、愚蠢的组件。
OrderList.razor
<div>
@foreach (var order in Orders)
{
<OrderItem Order="order" OnOrderClicked="OnOrderClicked.InvokeAsync" />
}
</div>
@code {
[Parameter]
public List<Order> Orders { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
}
现在 OrderItem 列表可以呈现有关订单的内容并充当点击目标,当订单被点击时,它会一直调用委托返回到基础 class,这就是那里该方法运行。 OrderClicked 方法还检查 EventCallback,因此如果没有分配委托,则单击不会执行任何操作。
OrderItem.razor
<div @onclick="OrderClicked">
<p>Order Name: @Order.OrderName</p>
</div>
@code {
[Parameter]
public Order Order { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
private void OrderClicked()
{
if(OnOrderClicked.HasDelegate)
{
OnOrderClicked.InvokeAsync(Order);
}
}
}
所有这些组合在一起构成了显示订单的组件,如果您单击打开的订单,它会移动关闭的订单列表,反之亦然。所有的逻辑都在基础 class 中,每个组件都有一个简单的工作要做,这使得推理变得容易得多。
这也会让我知道何时需要将一个组件分解成更小的组件。我的理念是不要一次向用户展示太多内容,因此每个页面都应该简洁、简单,并且不要期望做太多事情。为此,当我构建这样的东西时,当我的基础 class 或父 UI razor 文件开始膨胀时,我可以说我会走得很远,这会提示重构部分另一个专用页面的功能。它可以生成更多文件,但也使构建和维护变得更加容易。
这是对一个简短问题的长回答。你可能同意我的看法,也可能不同意,但希望它能帮助你决定如何继续进行。