• 小酌重构系列[24]——封装集合


    概述

    当方法返回类型或属性类型为集合时,有些开发者会千篇一律地使用IList<T>集合。然而IList<T>具有集合的所有操作,这意味着调用者不仅可以读取集合信息,还能够修改集合。业务需求本来只是为调用者提供一个可读的集合,例如数据的查询和展示,但当方法返回IList<T>时,无疑隐式地开放了集合可写的权限。此时,我们无法阻止调用者篡改集合元素。

    注意:将属性设定为IList<T>类型时,即使声明为只读的,我们仍然无法避免集合元素的篡改。
    例如public IList<Person> People{get; private set;},People属性是一个IList集合,它虽然是只读的,但是People集合里面的元素可以被修改。

    这种情况下,我们应该使用IEnumerable<T>来代替IList<T>。
    IList<T>和IEnumerable<T>都可以遍历集合的元素,IList<T>拥有集合的所有操作方法,包括集合元素的增加、修改和删除。
    而IEnumerable<T>则只有一个GetEnumerator方法(扩展方法除外),它返回一个可用于循环访问集合的IEnumerator<T>对象。

    示例

    重构前

    下面这段代码定义了两个类:Order和OrderLine。

    image

    • OrderLines属性是只读的
    • Order提供了AddOrderLine()和RemoveOrderLine()方法,用于添加和删除订单明细

    当调用者在取到Order实例时,仍然可以通过IList<T>的Add()或Remove()方法修改OrderLines集合。
    假设调用者篡改了OrderLines中的元素,并且通过另外的方法回传了Order实例,则可能会产生一些bug。

    /// <summary>
    /// 订单
    /// </summary>
    public class Order
    {
        private readonly List<OrderLine> _orderLines = new List<OrderLine>();
        private double _orderTotal;
    
        public IList<OrderLine> OrderLines
        {
            get { return _orderLines; }
        }
    
        public void AddOrderLine(OrderLine orderLine)
        {
            _orderTotal += orderLine.Total;
            _orderLines.Add(orderLine);
        }
    
        public void RemoveOrderLine(OrderLine orderLine)
        {
            orderLine = _orderLines.Find(item => item == orderLine);
            if (orderLine == null)
                return;
    
            _orderTotal -= orderLine.Total;
            _orderLines.Remove(orderLine);
        }
    }
    
    /// <summary>
    /// 订单明细
    /// </summary>
    public class OrderLine
    {
        public double Total { get; set; }
    }

    重构后

    重构后,不仅OrderLines集合是只读的,而且也只能通过AddOrderLine()和RemoveOrderLine()方法来增加或删除订单明细。

    /// <summary>
    /// 订单
    /// </summary>
    public class Order
    {
        private readonly List<OrderLine> _orderLines;
        private double _orderTotal;
    
        public Order(List<OrderLine> orderLines)
        {
            _orderLines = orderLines;
        }
    
        /// <summary>
        /// 订单明细集合是只读的,只能通过AddOrderLine()和RemoveOrderLine()来增加或删除订单明细
        /// </summary>
        public IEnumerable<OrderLine> OrderLines
        {
            get { return _orderLines; }
        }
    
        public double OrderTotal
        {
            get { return _orderTotal; }
        }
    
        public void AddOrderLine(OrderLine orderLine)
        {
            _orderTotal += orderLine.Total;
            _orderLines.Add(orderLine);
        }
    
        public void RemoveOrderLine(OrderLine orderLine)
        {
            orderLine = _orderLines.Find(item => item == orderLine);
            if (orderLine == null)
                return;
    
            _orderTotal -= orderLine.Total;
            _orderLines.Remove(orderLine);
        }
    }
    
    /// <summary>
    /// 订单明细
    /// </summary>
    public class OrderLine
    {
        public double Total { get; set; }
    }
    

    注意:上述代码有一些瑕疵,OrderLine类没有重写Object类的Equals()、GetHashCode()方法。
    这行代码orderLine = _orderLines.Find(item => item == orderLine)会使得orderLine每次都是null。
    较为完整的OrderLine如下:

    public class OrderLine
    {
        public int Id { get; set; }
        public int OrderId { get; set; }
        public double Total { get; set; }
    
    
        /// <summary>
        /// 重写Equals方法
        /// </summary>
        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is OrderLine))
                return false;
    
            if (ReferenceEquals(this, obj))
                return true;
    
            var other = (OrderLine) obj;
            if (IsTransient() && other.IsTransient())
                return false;
    
            var typeOfThis = GetType();
            var typeOfOther = other.GetType();
    
            if (!typeOfThis.IsAssignableFrom(typeOfOther) && !typeOfOther.IsAssignableFrom(typeOfThis))
            {
                return false;
            }
    
            return Id.Equals(other.Id);
        }
    
        /// <summary>
        /// 重写GetHashCode方法
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    
        /// <summary>
        /// 是否为瞬时对象
        /// </summary>
        /// <returns></returns>
        public virtual bool IsTransient()
        {
            return EqualityComparer<int>.Default.Equals(Id, default(int));
        }
    
        /// <summary>
        /// 提供==操作符,可用于对象比较
        /// </summary>
        public static bool operator ==(OrderLine left, OrderLine right)
        {
            if (Equals(left, null))
            {
                return Equals(right, null);
            }
    
            return left.Equals(right);
        }
    
        /// <summary>
        /// 提供!=操作用,可用于对象比较
        /// </summary>
        public static bool operator !=(OrderLine left, OrderLine right)
        {
            return !(left == right);
        }
    }
    

    小结

    当集合作为返回参数时,应使用适合业务需求的集合类型,不宜提供过多的集合操作给调用者。

  • 相关阅读:
    数据结构与算法分析-Code Blocks中出现的找不到头文件的问题
    数据结构与算法分析-用C语言实现栈(数组方式)
    数据结构与算法分析-用C语言实现栈(链表方式)
    数据结构与算法分析-用C语言实现单链表
    C语言经典算法100例-结束语
    C++ Primer 7.33 练习编写成员函数
    C语言经典算法100例-073-链表逆序插入节点
    C语言经典算法100例-072-创建一个链表
    LintCode-编辑距离
    LintCode-乘积最大子序列
  • 原文地址:https://www.cnblogs.com/keepfool/p/5548305.html
Copyright © 2020-2023  润新知