【问题标题】:In a Blazor app, what is best practice for separation of concerns and single responsibility principles? [closed]在 Blazor 应用程序中,关注点分离和单一职责原则的最佳实践是什么? [关闭]
【发布时间】:2020-10-10 00:13:25
【问题描述】:

我正在使用从派生自 ComponentBase 的基础继承的 Razor 文件。我的理解是,这两个文件一般应该负责处理UI相关的任务。话虽如此,我应该将我的数据服务调用放在低级组件中吗?还是应该将对它们的调用保留在可以编排数据服务的更高级别的组件中,然后简单地将数据传递给组件以处理渲染? (当我提到高或低级别时,我的意思是父组件将是高级别,而孙子将是低级别)

据我了解,为处理数据服务而注入的接口将拥有相同的资源(作为单例)。所以我的问题不是关于资源的管理,而是关于保持事物稳定。应该在哪里使用数据服务?无处不在还是与世隔绝?谢谢!

【问题讨论】:

    标签: blazor blazor-server-side solid-principles asp.net-blazor blazor-webassembly


    【解决方案1】:

    我将积极支持将服务隔离到基类。在得出这个结论之前,我一直遇到的问题是,随着应用程序大小和复杂性的增加,将服务调用分散到各处会变得令人困惑。将每个组件构建为一个原子的东西来处理它自己的所有事情并注入它的服务是非常诱人的,但是一旦所有这些组件开始组合在一起并且需要开始相互通信,它就会变得非常令人头疼。当你有类似单例的东西时,这会复合,其中可能涉及任何状态,因为组件的底层状态可以很容易地被另一个组件更改。 (有时不是故意的 - 请参阅 EF Core 和数据跟踪以及当被跟踪的数据从 2 个组件引用时您可以获得的乐趣 - 或者更糟糕的是,Blazor 服务器上的 2 个单独的客户端连接)在不知不觉中,有太多的地方在需要进行更改时查找错误或进行更改,并且跟踪错误变得像噩梦一样。

    实现组件自治的第二条途径是使用级联参数,但无论何时,您都是在将组件耦合到 DOM 树上某处的具体组件,避免耦合是 SOLID 的重点。通常最好让每个组件都代表非常简单的功能,这些功能可以组合起来为用户创造更丰富的体验。

    因此,我发现成功的地方是隔离您在基类中提到的服务,然后将 DOM 树中的每个组件都保持在尽可能低的位置,这对我的输出和查找能力产生了巨大的影响并修复错误。事实上,在我开始这种方法之前,我有一个项目不得不放弃两次,现在我在一个功能性应用程序中,并以一个很好的剪辑构建功能。 (感谢上帝,这是一个爱好项目!)

    实现这一点的方法一点也不复杂。在基类中,我将在需要时将方法调用和属性公开为受保护的,并尽可能保持其他所有内容为私有,因此外部可见性处于绝对最低限度。所有服务调用也发生在基类中,并封装在私有方法中,这会破坏服务和 UI 之间的连接。然后,我会将数据作为组件参数向下传递到 DOM 树,并将功能作为 EventCallback<T> 类型的参数向下传递。

    以经典的订单列表为例。我可以通过客户 ID 加载订单列表,然后通过使用表达式主体成员过滤主列表来公开打开的订单和关闭的订单的列表。所有这些都发生在基类中,但我对其进行了设置,因此 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");
        }
    }
    

    现在我可以使用顶级组件中的基类,并且只能访问受保护的和公共的成员。我可以使用这个高级组件来编排 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 组件随后负责呈现 OrderItem 列表并传递委托操作。同样,只是一个简单的、愚蠢的组件。

    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 列表可以呈现有关订单的内容并充当点击目标,当点击订单时,它会一直调用委托返回到基类,这就是方法运行的地方。 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);
            }
        }
    }
    

    所有这些共同构成显示订单的组件,如果您单击未结订单,它会移动已关闭订单列表,反之亦然。所有逻辑都在基类中,每个组件都有一个简单的工作要做,这使得推理变得更加容易。

    这也可以让我知道何时需要将组件分解为更小的组件。我的理念是不应该一次向用户展示太多内容,因此每个页面都应该简洁、简单,并且不要期望做太多事情。为此,当我构建这样的东西时,我可以告诉我,当我的基类或父 UI 剃须刀文件开始膨胀时,我会走得很远,这会促使将部分功能重构到另一个专用页面。它可以生成更多文件,但也使构建和维护变得更加容易。

    结果证明这是一个简短问题的长答案。您可能同意我的观点,也可能不同意,但希望它可以帮助您决定如何进行。

    【讨论】:

    • 感谢您抽出宝贵时间讨论您所走的道路及其原因。我相信我正在努力在我的解决方案中找到答案,因为在不同的情况下答案似乎是不同的。例如,在上面的示例中,我没有看到对数据服务的任何调用,但我确实看到了 Base 类中加载的数据。在您的示例中,这本质上是您调用数据服务的地方。但是,如果您改为在 OrderItem 中调用该数据服务呢?然后每个 OrderItem 将负责检索自己的数据。在某些情况下,这可能很昂贵。谢谢!
    • 我尽量避免在每个 OrderItem 中使用该服务,首先因为它违反了 SRP - 订单项会显示信息并处理数据。其次,传播逻辑代码和服务调用使维护和调试变得更加困难。您是正确的,因为我正在使用基类 fer 服务调用,并且任何数据转换逻辑也将在那里。这对我很有帮助。
    • 所以我正在考虑通过实现 ViewModel 来保持 razor 文件和基类服务都是免费的。这样做将允许基类仅处理与 UI 相关的任务。 razor 文件和基类都可以访问 ViewModel,可能通过 DI。 ViewModel 可以处理更多的业务逻辑。这最初对我来说似乎有点矫枉过正,但我​​会试一试。我越想越有意义。
    • 我已经这样做并且成功了,但我需要一种方法将属性更改通知返回到 UI,以便在正确的时间进行更新。 Here is a great blog post 使用 View Regions 和 INotifyPropertyChanged 来做这件事。希望对你有帮助,祝你好运!
    猜你喜欢
    • 1970-01-01
    • 2010-12-16
    • 2012-05-24
    • 2019-11-15
    • 1970-01-01
    • 2014-07-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多