How to enforce rule-based object ordering when inserting into a list

风流意气都作罢 提交于 2020-01-15 11:29:08

问题


I have an interface IRenderable and a class that manages rendering all instances of classes implementing the interface.

I have many classes in my code and will expect others to create classes that implement the same interface.

Due to the nature of rendering I want to say things like "Draw instances of this class before instances of this class".

The typical approach is to have every class implement a DrawOrder property, however I dislike this because classes don't have a definite draw order value, it's the relative order that matters. If I gave every class a DrawOrder property anyone implementing the interface would need to know what the values of all classes were. Obviously this isn't possible if many people could implement their own class.


What I'd like is to be able to define rules that say "ClassA before ClassB, ClassC before ClassA", then when working out the draw order / adding instances I could infer a correct drawing order. Others implementing the interface could add their own rules relating to built in implementations and their own additions.


EDIT: What I'm hoping for is some class that manages the rules and manages maintaining an order, something like below:

class Renderer
{
    private List<Rule> Rules;

    private List<IRenderable> Renderables;

    // Adds to list of rules
    void EnforceBefore(Type FirstClass, Type SecondClass);

    // Inserts items ensuring all rules are followed.
    void Insert(IRenderable ClassInstance);

    void RenderAll();
}

Classes could then add rules as appropriate (or I could have an interface method that returns them).

Below is a quick test that doesn't work

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        List<string> MyList = new List<string> { "wherever", "second", "first", "third", "first", "third", "second" };

        RuleBasedComparer<string> RuleComparer = new RuleBasedComparer<string>();

        // I want to ensure all instances of "first" appear in the list before all instances of "second"
        // and all instances of "second" appear in the list before all instances of "third".
        // I don't care where "wherever" appears (or anything beyond the above rules)

        RuleComparer.AddRule("first", "second");
        RuleComparer.AddRule("second", "third");

        MyList.Sort(RuleComparer);

        foreach (var item in MyList)
            Console.WriteLine(item);
        Console.ReadKey();
    }
}

public class RuleBasedComparer<T> : Comparer<T>
{
    private class OrderRule
    {
        public readonly T Before;
        public readonly T After;

        public OrderRule(T before, T after)
        {
            Before = before;
            After = after;
        }
    }

    private List<OrderRule> _Rules = new List<OrderRule>();

    public void AddRule(T before, T after)
    {
        _Rules.Add(new OrderRule(before, after));
    }

    public override int Compare(T x, T y)
    {
        // Find the applicable rule for this pair (if any)
        var ApplicableRule = _Rules.Where(or => or.After.Equals(x) && or.Before.Equals(y) ||
                                                or.After.Equals(y) && or.Before.Equals(x)).SingleOrDefault();

        if (ApplicableRule != null)
        {
            // If there is a rule then if x should be before y then return -1, otherwise return 1
            if (ApplicableRule.Before.Equals(x))
                return -1;
            else
                return 1;
        }
        else
        {
            // If no rule exists then say they are equal
            return 0;
        }
    }
}

TL;DR: How do I go from rules that say things like "ClassA before ClassB" to a definite ordering of instances/classes.
Ambiguities created by a lack of complete rules shouldn't matter, I just want the existing rules adhered to.


回答1:


You could add two attributes, that would take a type as parameter, and require the target class to be IRenderable.

[Before(typeof(MyClass))]

[After(typeof(MyClass))]

Then you'd have to fetch all classes that implement IRenderable and retrieve those attributes.

Then all you have to do is sort them in the right order (i'm relying on an algorithm pro to come up with a fancy algorithm name on this one).

You'll have to decide what to do if the result is ambiguous (i.e: two classes are after the same one).

Of course you could implement a similar logic through the same interface but this goes slightly out of the scope of IRenderable's duty in my opinion so i'd make another one.

HTH,

Bab.




回答2:


Can the renderer determine the ordering of all classes, even those that aren't implemented yet? If so, then you can create an IComparer<IRenderable>. If you don't know what the desired order of classes implemented in the future is, then this is no more feasible than relying on the implementers to specify the draw order explicitly!

Normally for e.g. rendering, you can often determine the draw order from properties of the objects, so you could have a comparer that decides render order based on known rendering properties such as transparency, ui-layer and so on.

public class RenderOrderComparer : IComparer<IRenderable>
{
    public int Compare(IRenderable a, IRenderable b)
    {
       if (a.IsOverlay && !b.IsOverlay)
         return -1;
       if (b.IsOverlay && !a.IsOverlay)
         return 1,
       if (a.IsTransparent && !b.IsTransparent)
         return -1;
       if (b.IsTransparent && !a.IsTransparent)
         return 1;
       // ...and so on.

       return 0;
    } 
}

(Note that you should consider extending Comparer rather than implementing IComparer, see the MSDN Documentation on IComparer)

The other option is a more rule based per-class solution where you let all classes decide by implementing the IComparable interface. This gives each class the opportunity to order itself before/after other (known) classes. The drawback here is of course that it can't provide rules for classes it doesn't know exist. Also, you can't be sure that ClassA says it should render before ClassB, while ClassB says it should render before ClassA. A scheme like this demands that you maintain the ordering for all types, to see that it is consistent. Basically you end up with a comparer implementation distributed over all your classes, which might be a bad thing.

public interface IRenderable : IComparable<IRenderable>
{
    int Id { get; }
}

public class SomeRenderable : IRenderable
{
   public int CompareTo(IRenderable other)
   {
      if (other is SomeRenderable)
         return 0;
      if (other is OtherRenderableType)
         return 1;   
   }
}



回答3:


I've managed to figure out a method that seems to work (complete code example below).

Basically I add my rules, then I assign everything included in a rule with a possible ordering by inserting them into a list one by one, observing the rules as I add each one. The position of the item in the list becomes the ordering. When comparing 2 things in the list (not in a rule) I just return 0.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        List<string> MyList = new List<string> { "second", "first", "second2", "wherever", "third", "second2", "third", "second", "first" };

        RuleBasedComparer<string> RuleComparer = new RuleBasedComparer<string>();

        // I want to ensure all instances of "first" appear in the list before all instances of "second" and "second2"
        // and all instances of "second" and "second2" appear in the list before all instances of "third".
        // I don't care where "wherever" appears (or anything beyond the above rules)

        RuleComparer.AddRule("first", "second");
        RuleComparer.AddRule("first", "second2");
        RuleComparer.AddRule("second", "third");
        RuleComparer.AddRule("second2", "third");

        MyList.Sort(RuleComparer);

        foreach (var item in MyList)
            Console.WriteLine(item);
        Console.ReadKey();
    }
}

public class RuleBasedComparer<T> : Comparer<T>
{
    private class OrderRule
    {
        public readonly T Before;
        public readonly T After;

        public OrderRule(T before, T after)
        {
            Before = before;
            After = after;
        }
    }

    private List<OrderRule> _Rules = new List<OrderRule>();

    private List<T> DesiredOrdering = new List<T>();

    private bool _NeedToCalculateOrdering = true;

    public void AddRule(T before, T after)
    {
        if (!_NeedToCalculateOrdering)
            throw new InvalidOperationException("Cannot add rules once this comparer has.");

        _Rules.Add(new OrderRule(before, after));
    }

    private void CalculateOrdering()
    {
        _NeedToCalculateOrdering = false;

        var ItemsToOrder = _Rules.SelectMany(r => new[] { r.Before, r.After }).Distinct();


        foreach (var ItemToOrder in ItemsToOrder)
        {
            var MinIndex = 0;
            var MaxIndex = DesiredOrdering.Count;

            foreach (var Rule in _Rules.Where(r => r.Before.Equals(ItemToOrder)))
            {
                var indexofAfter = DesiredOrdering.IndexOf(Rule.After);

                if (indexofAfter != -1)
                {
                    MaxIndex = Math.Min(MaxIndex, indexofAfter);
                }
            }

            foreach (var Rule in _Rules.Where(r => r.After.Equals(ItemToOrder)))
            {
                var indexofBefore = DesiredOrdering.IndexOf(Rule.Before);

                if (indexofBefore != -1)
                {
                    MinIndex = Math.Max(MinIndex, indexofBefore + 1);
                }
            }

            if (MinIndex > MaxIndex)
                throw new InvalidOperationException("Invalid combination of rules found!");

            DesiredOrdering.Insert(MinIndex, ItemToOrder);
        }
    }

    public override int Compare(T x, T y)
    {
        if (_NeedToCalculateOrdering)
            CalculateOrdering();

        if (x == null && y != null)
        {
            return -1;
        }
        else if (x != null && y == null)
            return 1;
        else if (x == null && y == null)
            return 0;

        // Find the applicable rule for this pair (if any)
        var IndexOfX = DesiredOrdering.IndexOf(x);
        var IndexOfY = DesiredOrdering.IndexOf(y);

        if (IndexOfX != -1 && IndexOfY != -1)
        {
            // We have a definite order
            if (IndexOfX > IndexOfY)
                return 1;
            else if (IndexOfX < IndexOfY)
                return -1;
            else
                return 0;
        }
        else if (IndexOfX != -1)
        {
            return -1;
        }
        else if (IndexOfY != -1)
        {
            return 1;
        }
        else
        {
            return 0; // Or maybe compare x to y directly
            //return Comparer<T>.Default.Compare(x, y);
        }
    }
}


来源:https://stackoverflow.com/questions/9886148/how-to-enforce-rule-based-object-ordering-when-inserting-into-a-list

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!