適用於MES、WMS、ERP等管理系統的實體下拉框設計

2022-07-14 18:00:13

場景

該設計多適用於MESERPWMS 等管理型別的專案。

在做管理型別專案的時候,前端經常會使用到下拉框,比如:裝置,工廠等等。下拉元件中一般只需要他們的ID,Name屬性,而這些資料都建立於其他模組並儲存在資料庫中。

如圖:

寫一個裝置的下拉元件的資料需要通過請求後端去獲取,如:localhost:5555/api/services/app/Resource/GetNdoCombox,然後攜帶引數filterText

寫一個工廠的下拉元件也是同樣的要去請求,如:localhost:5555/api/services/app/Factory/GetNdoCombox

如果你的後端程式碼足夠規範,那麼就會像我寫的這樣,每個需要有下拉需求的實體建模都有一個GetNdoCombox的介面。

問題點

為了程式碼的複用性,你一定不會每個用到裝置下拉的地方都去寫select標籤,然後請求資料再渲染,而是會封裝一個裝置下拉框和一個工廠下拉框。於是便出現了一些問題:

前端下拉元件除了請求的介面不一樣,placeholder不一樣,其他的程式碼都是盡數相同的。如果有下拉需求的地方多了,就會出現很多個xx下拉元件,如何優化?如果你有此類相同需求的問題時值得參考這個方案。

方案

思路

在後端寫一個介面做統一查詢,前端做一個統一的下拉元件,請求後端的統一查詢介面,前端傳遞標識性的引數給後端,後端通過引數自動匹配並查詢前端所需要的值。那麼重點就在後端如何實現這樣的匹配邏輯呢?

核心實現

我的實踐架構:.NET CORE + VUE

前端

前端就是寫個元件去請求介面,傳遞引數,這裡就不細說了

後端

先粗淺的介紹需要準備的東西:

  1. 自定義服務描述類,包含:服務介面,服務實現,泛型實體型別,生命週期
  2. 定義單例記憶體:定義Ndo字典,用於儲存對應的服務描述,以實體完全限定名為key,定義存和取的方法
  3. 通過反射獲取指定程式集中實現了IComboxQuery介面並且必須實現了IComboxQuery<>的下拉服務的服務描述,再將其注入到IOC容器中,並同時在記憶體的字典中新增對映關係。
  4. 統一獲取服務的Hub:從記憶體中根據實體名稱獲取對應的服務描述,再根據自定義服務描述類中的服務介面型別從IOC容器中獲取實現的IComboxQuery

詳細說明

Ndo:其實就是普通的實體的意思。

首先通過提供的IComboxQuery介面和IComboxQuery介面約束ControllerService必須實現GetNdoCombox方法。也就是說所有需要下拉的實體的服務都要實現IComboxQuery。(IComboxQuery繼承於IComboxQuery)

程式啟動時利用反射將實現了IComboxQuery並且實現了IComboxQuery的服務新增到IOC容器和記憶體的字典中去,以實體完全限定名為key,value為自定義的服務描述類。

定義統一獲取服務的Hub,從記憶體中根據實體名稱獲取對應的服務描述,再根據自定義服務描述類中的服務介面型別從IOC容器中獲取實現IComboxQuery的ControllerService,然後呼叫GetNdoCombox方法

定義統一的ControllerService,隨便定義一個方法定義需要的引數為EntityNamefilterText,方法中使用統一獲取服務的Hub,通過引數EntityName獲取實際實現IComboxQuery的物件,然後呼叫GetNdoCombox返回資料。

核心的查詢邏輯仍然是由服務自己實現的,因為不同的實體,過濾條件的欄位名不一樣,Hub只負責呼叫方法得到結果,不關心具體實現。

程式碼

返回的資料NdoDto

public class NdoDto
{
    public virtual Guid Id { get; set; }
    public virtual string Name { get; set; }
    public virtual DateTime CreationTime { get; set; }
}

公共介面查詢的引數類

/// <summary>
/// 下拉框查詢的模糊搜尋輸入
/// </summary>
public class GetQueryFilterInput
{
    /// <summary>
    /// 型別全名稱,不涉及業務,用於區分本次請求是哪個實體的介面
    /// </summary>
    public virtual string Name { get; set; }
    /// <summary>
    /// 模糊查詢
    /// </summary>
    public virtual string FilterText { get; set; }
}

統一規範的公共介面

/// <summary>
/// 下拉查詢
/// </summary>
public interface IComboxQuery
{
    Task<List<NdoDto>> GetCombox(GetQueryFilterInput input);
}

/// <summary>
/// 下拉查詢
/// </summary>
public interface IComboxQuery<TEntity> : IComboxQuery
{

}

自定義的服務對映描述類

/// <summary>
    /// 服務對映描述
    /// </summary>
    public class SampleServiceDescriptor
    {
        /// <summary>
        /// 瞬時依賴注入服務介面
        /// </summary>
        public static Type TransientInterface { get; } = typeof(ITransientDependency);
        /// <summary>
        /// 單例依賴注入服務介面
        /// </summary>
        public static Type SingletonInterface { get; } = typeof(ISingletonDependency);

        /// <summary>
        /// 服務型別 介面
        /// </summary>
        public virtual Type ServiceType { get; }

        /// <summary>
        /// 實現型別
        /// </summary>
        public virtual Type ImplementationType { get; }

        /// <summary>
        /// 建模實體型別
        /// </summary>
        public virtual Type EntityType { get; }

        /// <summary>
        /// 服務依賴注入生命週期
        /// </summary>
        public virtual ServiceLifetime ServiceLifetime { get; }

        /// <summary>
        /// 依賴注入服務
        /// </summary>
        /// <param name="serviceType">服務型別</param>
        /// <param name="implementationType">實現型別</param>
        public SampleServiceDescriptor(Type serviceType, Type implementationType)
        {
            this.ServiceType = serviceType;
            this.ImplementationType = implementationType;

            if (serviceType != null && serviceType.GenericTypeArguments.Length > 0)
            {
                // 獲取IComboxQuery<>中的泛型引數TEntity
                this.EntityType = serviceType.GenericTypeArguments[0];
            }


            if (SingletonInterface.IsAssignableFrom(this.ImplementationType))
            {
                this.ServiceLifetime = ServiceLifetime.Singleton;
            }
            else
            {
                this.ServiceLifetime = ServiceLifetime.Transient;
            }
        }

        /// <summary>
        /// 轉換為 <see cref="ServiceDescriptor"/>
        /// </summary>
        /// <returns></returns>
        public ServiceDescriptor ToServiceDescriptor()
        {
            return new ServiceDescriptor(this.ServiceType, this.ImplementationType, this.ServiceLifetime);
        }
    }

程式啟動時的掃描器(反射獲取實現了介面的服務)

/// <summary>
/// 依賴注入服務描述器
/// </summary>
public static class SampleServiceDescriptorHelper
{
    /// <summary>
    /// 掃描程式集中的某個介面的實現
    /// </summary>
    /// <param name="interfaceType">介面</param>
    /// <param name="genericInterfaceTypes">介面泛型實現</param>
    /// <param name="assemblies">程式集列表</param>
    /// <returns></returns>
    public static IEnumerable<SampleServiceDescriptor> ScanAssembliesServices
        (Type interfaceType, IEnumerable<Type> genericInterfaceTypes, params Assembly[] assemblies)
    {
        // 泛型介面轉字典
        var genericInterfaceTypeDict = new Dictionary<Type, bool>();
        foreach (var item in genericInterfaceTypes)
        {
            genericInterfaceTypeDict[item] = true;
        }

        // 遍歷程式集中所有的符合條件的型別
        foreach (var assembly in assemblies)
        {
            var services = assembly.GetTypes()
                .Where(o => interfaceType.IsAssignableFrom(o)
                       && o.IsPublic
                       && !o.IsInterface
                       && !o.IsAbstract
                      )
                .Select(o =>
                        {
                            // 篩選某個介面
                            var entityInterfaceType = o.GetInterfaces()
                                .Where(x =>
                                       {
                                           if (!x.IsGenericType)
                                           {
                                               return false;
                                           }
                                           var genericTypeDefinition = x.GetGenericTypeDefinition();

                                           return genericInterfaceTypeDict.ContainsKey(genericTypeDefinition);
                                       })
                                .FirstOrDefault();
                            // entityInterfaceType = IComboxQuery<> 目前只有一種
                            return new SampleServiceDescriptor(entityInterfaceType, o);
                        })
                .Where(o => o != null && o.ServiceType != null);

            foreach (var service in services)
            {
                yield return service;
            }
        }
    }
    // interfaceType用於獲取所有實現了IComboxQuery的型別
    // genericInterfaceTypes用於篩選,必須要實現了IComboxQuery<>的型別,因為需要獲取其TEntity的型別
    // 如果只是實現了IComboxQuery的型別,是沒有TEntity的,會導致ComboxQueryInfoStorage中無法新增對映關係
}

單例的記憶體

public class ComboxQueryInfoStorage : IComboxQueryInfoStorage
{
    /// <summary>
    ///  ModelingComboxQueryInfo 記憶體範例
    /// </summary>
    public static IComboxQueryInfoStorage Instace { get; set; } = new ComboxQueryInfoStorage();

    /// <summary>
    /// 資料記憶體
    /// </summary>
    protected readonly Dictionary<string, SampleServiceDescriptor> _Dict;

    protected ComboxQueryInfoStorage()
    {
        this._Dict = new Dictionary<string, SampleServiceDescriptor>();
    }

    public void Add(params SampleServiceDescriptor[] comboxQueryInfos)
    {
        foreach (var item in comboxQueryInfos)
        {
            this._Dict[item.EntityType.FullName] = item;
        }
    }

    public SampleServiceDescriptor Get(string name)
    {
        if (this._Dict.TryGetValue(name,out var comboxQueryInfo))
        {
            return comboxQueryInfo;
        }
        throw new Exception($"found Ndo type: {name}");
    }
}

統一獲取服務的Hub

public class ComboxQueryHub : IComboxQueryHub
{
    /// <summary>
    /// 依賴注入容器
    /// </summary>
    protected readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="serviceProvider"></param>
    public ComboxQueryHub(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task<List<NdoDto>> GetCombox(GetQueryFilterInput input)
    {
        var comboxQuery = this.GetComboxQuery(input.Name);
        return await comboxQuery.GetCombox(input);
    }

    public IComboxQuery GetComboxQuery(string name)
    {
        var comboxQueryInfo = ComboxQueryInfoStorage.Instace.Get(name);
        var comboxQuery = _serviceProvider.GetService(comboxQueryInfo.ServiceType) as IComboxQuery;
        return comboxQuery;
    }
}

用於將服務註冊到IOC和記憶體的擴充套件類

public static class ComboxQueryExtensions
{
    /// <summary>
    /// ComboxQuery 介面型別
    /// </summary>
    public static Type InterfaceType { get; } = typeof(IComboxQuery);

    /// <summary>
    /// IComboxQuery 介面型別
    /// </summary>
    public static List<Type> GenericInterfaceTypes { get; } = new List<Type>()
    {
        typeof(IComboxQuery<>)
    };

    /// <summary>
    /// 註冊程式集中的 ComboxQuery
    /// </summary>
    /// <returns></returns>

    public static void RegistrarComboxQuery(this IServiceCollection services, params Assembly[] assemblies)
    {
        // query hub
        if (!services.Any(x=>x.ServiceType == typeof(IComboxQueryHub)))
        {
            services.AddTransient<IComboxQueryHub, ComboxQueryHub>();
        }

        // querys
        var sampleServiceDescriptors = ScanComboxQuerys(assemblies);
        foreach (var sampleServiceDescriptor in sampleServiceDescriptors)
        {
            if (services.Any(x => x.ServiceType == sampleServiceDescriptor.ServiceType))
            {
                continue;
            }

            ComboxQueryInfoStorage.Instace.Add(sampleServiceDescriptor);

            if (sampleServiceDescriptor.ServiceLifetime == ServiceLifetime.Singleton)
            {
                services.AddSingleton(sampleServiceDescriptor.ServiceType,sampleServiceDescriptor.ImplementationType);
            }
            else
            {
                services.AddTransient(sampleServiceDescriptor.ServiceType, sampleServiceDescriptor.ImplementationType);
            }
        }
    }

    /// <summary>
    /// 掃描程式集中的 ComboxQuery 實現
    /// </summary>
    /// <param name="assemblies"></param>
    /// <returns></returns>
    public static IEnumerable<SampleServiceDescriptor> ScanComboxQuerys(params Assembly[] assemblies)
    {
        return SampleServiceDescriptorHelper.ScanAssembliesServices(
            InterfaceType,
            GenericInterfaceTypes,
            assemblies
        );
    }
}

使用

在啟動類中註冊服務

builder.Services.RegistrarComboxQuery(typeof(Program).Assembly);

人員建模:PersonController

public class PersonController : ApiControllerBase, IComboxQuery<Person>
{
    [HttpPost]
    public Task<List<NdoDto>> GetCombox(GetQueryFilterInput input)
    {
        var persons = Person.GetPeoples();
        var ndos = persons.Select(x => new NdoDto
                                  {
                                      Id = x.Id,
                                      Name = x.PersonName,
                                  }).ToList();
        return Task.FromResult(ndos);
    }
}

裝置建模:ResourceController

public class ResourceController : ApiControllerBase, IComboxQuery<Resource>
{
    [HttpPost]
    public Task<List<NdoDto>> GetCombox(GetQueryFilterInput input)
    {
        var resources = Resource.GetResources();
        var ndos = resources.Select(x => new NdoDto
                                    {
                                        Id = x.Id,
                                        Name = x.ResourceName
                                    }).ToList();
        return Task.FromResult(ndos);
    }
}

統一查詢介面:CommonBoxController

public class CommonBoxController : ApiControllerBase
{
    /// <summary>
    /// ioc容器
    /// </summary>
    protected readonly IServiceProvider _serviceProvider;

    public CommonBoxController(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    [HttpPost]
    public virtual async Task<List<NdoDto>> GetNdoCombox(GetQueryFilterInput input)
    {
        var queryHub = this._serviceProvider.GetService<IComboxQueryHub>();

        return await queryHub.GetCombox(input);
    }
}

效果

單獨請求PersonController

單獨請求ResourceController

請求公共介面CommonBoxController

程式碼倉庫

地址:https://gitee.com/huang-yuxiang/common-combox/tree/main/

版權宣告

作者:不想只會CURD的猿某人

更多原著文章請參考:https://www.cnblogs.com/hyx1229/