📜 ⬆️ ⬇️

How to improve Linker implementation in .NET


Each proger surely used the “Linker” pattern, and most of us also faced the need to implement it in our project. And often it turns out that each of its implementation imposes special requirements on the business logic to be defined, and from the point of view of working with a hierarchical structure, we want to have an equally wide range of possibilities: Add and Remove methods are often not enough, so why not add Contains, Clear and a dozen others? And if you still need special methods for traversing subtrees through iterators? And I want to have such functionality for various independent hierarchies, and also not to burden myself with the need to determine the implementation of such methods in each of the many Composite elements. Well, the sheet components, too, would not hurt to simplify.

Below, I will offer my own solution to this problem, as applied to C # features.


So, we have the interface type Component, which is overloaded with two areas of responsibility : one defines the business logic - that is why the hierarchy was built; the second provides transparent interaction in the hierarchy and manages descendants for composite elements. Let's try to put one of them into a separate interface, which can be accessed by the property of the Children component (or the GetChildren method). In the object that is returned by the property, all operations on the collection will be collected, including enumeration, addition and removal of child elements, as well as everything that we want.
')


We also defined the IsComposite property (method) to get a quick and readable check for whether the element is composite or sheet. You can not use this property: then, when you try to change the collection of child elements, a NotSupportedException will be thrown for the sheet component. Thus, we do not lose the interface transparency for all components - the main advantages of the “Linker” pattern - and at the same time we get an easy way to determine if any selected component can have child elements.

Now we will try to determine the implementation of the IComponent interface, which would be well adapted for possible changes, and therefore applicable for building various hierarchies.

using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace ComponentLibrary
{
[ContractClass( typeof (IComponentContract))]
public interface IComponent< out TComponent, out TChildrenCollection>
where TComponent : IComponent<TComponent, TChildrenCollection>
where TChildrenCollection : class , IEnumerable <TComponent>
{
TChildrenCollection Children { get ; }
bool IsComposite { get ; }
}
}


* This source code was highlighted with Source Code Highlighter .

What is the interface implemented as a template from and what kind of parameters does it take? In fact, the idea is very simple: to introduce strong typing where the actual types are not yet known.

TComponent is just the actual type of that child interface (or class in a more closely related architecture) that you inherit from IComponent to add Operation () responsibilities to it.

TChildrenCollection is the collection interface that you implement to access the children. In this interface, at least only GetEnumerator () should be defined, through which you can get a collection iterator. The fact is that sometimes it is not necessary to provide methods like Add () and Remove (), since all elements can be added in the constructor, and the hierarchical structure itself should not be changed after creation. And if you suddenly need a collection change notification, pass ObservableCollection <TComponent> as TChildrenCollection, and the trick is done!

In a type contract, we specify the following restrictions on the return value of the Children property: 1) the returned collection is not null; 2) none of its elements is null; 3) the component is either listed as composite or does not contain any child elements. For brevity, the contract code is not given here.

Remember, we said that we would like to endow each component with an additional method that returns an iterator that implements the complex logic of traversing a subtree? Suppose that we wrote such an iterator ComponentDescendantsEnumerator <TComponent> (its code can be downloaded from the link at the end of the article), and then wrapped it into the ComponentDescendantsEnumerable <TComponent> class, defining IEnumerable <TComponent>. Now you need to decide where to place the methods that return such iterators? Fortunately, C # has a very useful mechanism - extension methods. Let's try to apply it.

namespace ComponentLibrary.Extensions
{
public static class ComponentExtensions
{
public static IEnumerable <T> GetDescendants<T>( this T component)
where T : IComponent<T, IEnumerable <T>>
{
//
return new ComponentDescendantsEnumerable<T>(component);
}
}
}


* This source code was highlighted with Source Code Highlighter .

We implement an extension method in a separate namespace - so we can call it like a method belonging to the IComponent <,> interface only when we import this namespace.

Then we have another task: we need to implement a way to get collections that never contain elements, and for all change requests (like Add / Remove) throw a NotSupportedException. First, we will create one such collection that implements the ICollection <T>. However, if the collection never changes and is always created empty, then there is no point in doing more than one such collection for the entire program (or rather, on the AppDomain). The perfect occasion to take advantage of the Singleton pattern! (the implementation of the class Singleton <T> can be found in the attached source code)

sealed internal class ItemsNotSupportedCollection<T> :
Singleton<ItemsNotSupportedCollection<T>>,
ICollection<T>
{
private ItemsNotSupportedCollection() { }

public int Count { get { return 0; } }

public bool IsReadOnly { get { return true ; } }

public bool Contains(T item) { return false ; }

public void CopyTo(T[] array, int arrayIndex) { }

public void Add(T item) { throw new NotSupportedException(); }

public void Clear() { throw new NotSupportedException(); }

public bool Remove(T item) { throw new NotSupportedException(); }

public IEnumerator<T> GetEnumerator()
{
return ItemsNotSupportedEnumerator<T>.Instance;
}

IEnumerator IEnumerable .GetEnumerator()
{ return this .GetEnumerator(); }
}


* This source code was highlighted with Source Code Highlighter .

For the sake of transparency, the iterator class used represents the iterator of an empty collection, so one of its copies is also sufficient - again, Singleton.

sealed internal class ItemsNotSupportedEnumerator<T> :
Singleton<ItemsNotSupportedEnumerator<T>>,
IEnumerator<T>
{
private ItemsNotSupportedEnumerator() { }

public T Current { get { return default (T); } }

public void Dispose() { }

object IEnumerator.Current { get { return null ; } }

public bool MoveNext() { return false ; }

public void Reset() { throw new NotSupportedException(); }
}


* This source code was highlighted with Source Code Highlighter .

It remains only to create a static property that is visible from the outside of the assembly and returns a read-only collection of elements for the ICollection <T> interface.

public static class ComponentCollections<TComponent>
where TComponent : IComponent<TComponent, IEnumerable <TComponent>>
{
public static ICollection<TComponent> EmptyCollection
{
get
{
//
return ItemsNotSupportedCollection<TComponent>.Instance;
}
}
}


* This source code was highlighted with Source Code Highlighter .

The time has come for the most interesting: the use of our mini-library to create a specific class hierarchy. Suppose you need to organize a menu system consisting of: MenuCommand is a specific command, and Menu is a submenu that can contain other commands and submenus. All classes are located in a separate assembly. The sixth sense suggests that the “Linker” pattern would have been useful here.



First, we define an interface common to all classes of the hierarchy. Inside the interface, we define only the methods and properties required by the business logic of our menu (in this case, each element has a name and is able to display itself with the specified indentation).



namespace MenuLibrary
{
public interface IMenuItem :
IComponent<IMenuItem, ICollection<IMenuItem>>
{
string Name { get ; }
void Display( int indent = 0);
}
}


* This source code was highlighted with Source Code Highlighter .

As a TComponent parameter-type, we always pass the interface of the components (that is, the same IMenuItem), and the TChildrenCollection - the interface of the implemented collection. We could create a custom interface for the TChildrenCollection, which defines the Add, Remove and GetChild methods (and also GetEnumerator), as in the classic version of the pattern. You can send for example IList <IMenuItem>, but we decided that here the standard ICollection <IMenuItem> interface suits us.

This is how we define the MenuCommand leaf component:

public class MenuCommand : IMenuItem
{
private readonly string name;

public MenuCommand( string name)
{
//
this .name = name;
}

public string Name { get { return this .name; } }

public void Display( int indent = 0)
{
string indentString = MenuHelper.GetIndentString(indent);
Console .WriteLine( "{1}{0} [Command]" , this .name, indentString);
}

public ICollection<IMenuItem> Children
{
get { return ComponentCollections<IMenuItem>.EmptyCollection; }
}

public bool IsComposite { get { return false ; } }
}


* This source code was highlighted with Source Code Highlighter .

The Children property returns the previously declared “singleton” collection for leaf elements. Now we will declare the composite component Menu.

public class Menu : IMenuItem
{
private readonly ICollection<IMenuItem> children =
new List <IMenuItem>();

private readonly string name;

public Menu( string name)
{
//
this .name = name;
}

public string Name { get { return this .name; } }

public void Display( int indent = 0)
{
string indentString = MenuHelper.GetIndentString(indent);
Console .WriteLine( "{1}{0} [Menu]" , this .name, indentString);
int childrenIndent = indent + 1;
foreach (IMenuItem child in this .children)
{
child.Display(childrenIndent);
}
}

public ICollection<IMenuItem> Children
{ get { return this .children; } }

public bool IsComposite { get { return true ; } }
}


* This source code was highlighted with Source Code Highlighter .

Children unexpectedly returns the used standard List <T> collection. This is a good move if the user of our hierarchy is allowed to bring the type of the Children object to a List <T> and use all the advanced features of this class. But if such an alignment is not allowed, then you need to wrap List <T> into some internal class that implements only ICollection <T> and is not accessible to other assemblies (or even classes).

Now we will test the code written by us.

using System;
using System.Linq;
using ComponentLibrary.Extensions;
using MenuLibrary;
namespace MenuTest
{
public static class MenuTest
{
public static void Perform()
{
//
IMenuItem rootMenu = new Menu( "Root" );
// ... File
IMenuItem fileMenu = new Menu( "File" );
fileMenu.Children.Add( new MenuCommand( "New" ));
fileMenu.Children.Add( new MenuCommand( "Open" ));
// ... File->Export
IMenuItem fileExportMenu = new Menu( "Export" );
fileExportMenu.Children.Add( new MenuCommand( "Text Document" ));
fileExportMenu.Children.Add( new MenuCommand( "Binary Format" ));
fileMenu.Children.Add(fileExportMenu);
// ... File
fileMenu.Children.Add( new MenuCommand( "Exit" ));
rootMenu.Children.Add(fileMenu);
// ... Edit
IMenuItem editMenu = new Menu( "Edit" );
editMenu.Children.Add( new MenuCommand( "Cut" ));
editMenu.Children.Add( new MenuCommand( "Copy" ));
editMenu.Children.Add( new MenuCommand( "Paste" ));
rootMenu.Children.Add(editMenu);
//
rootMenu.Display();
Console .WriteLine();
// ,
// Root, "E" "R"
var compositeMenuNames =
from menu in rootMenu.GetDescendants()
where menu.IsComposite
&& (menu.Name.StartsWith( "E" ) || menu.Name.StartsWith( "R" ))
select menu.Name;
foreach ( string menuName in compositeMenuNames)
{
Console .WriteLine(menuName);
}
}
}
}


* This source code was highlighted with Source Code Highlighter .

Note the LINQ query on the enumeration returned by the GetDescendants () extension method. Let's look at the result of the work.



That's how simple it is. Moreover, in the logic of the developed components, the entire attention of the designer is focused on the construction of business logic, and not on the hierarchical structure or classes of containers.

Link to sources: http://www.fileden.com/files/2011/10/7/3205975/ComponentLibrary.zip

PS If something did not seem obvious to you, or you would like to look at a variant of the implementation of a hierarchy that is read-only, then you can refer to an extended version of the same post.

PPS To be honest, I hope that in the comments someone will offer a better implementation of Linker than me.

UPD As avalter correctly noted, here I just applied the Extract Interface, Extract Class to the Linker and used NullObject (it is advisable to finish the Extract Class, render the used collection of composite components and encapsulate it into a separate class). The result is not just Linker, but a more flexible structure.

I do not advise to implement the pattern in this way from scratch in any way! But you can take the ComponentLibrary assembly code, copy it into your project and automatically get some advantages for your hierarchy: ready-made Null-Object collections for leaf elements, additional iterators for circumventing the structure, as well as a contract for the IComponent interface that was mentioned briefly here. So when implementing a new hierarchy similar to IMenuItem, one can only think about the logic of the Display () methods, if the structure interfaces implemented in the ComponentLibrary are sufficient (otherwise define your implementations).

Source: https://habr.com/ru/post/130012/


All Articles