Patterns for controlling an object's public operations outside of it





5.00/5 (3 votes)
We frequently need to control (or limit) the public operations of an object based on the sate of another object (mostly the owner). I have listed four ways of dealing with such situations with a sample player, playlist scenario.
Introduction
We frequently need to control (or limit) the public operations of an object based on the current state of another object (mostly the owner).
For example, you have a playlist class that has some properties, methods and a collection of playlist items. You want to prevent manipulation of the playlist item collection if player is playing this playlist.
Sample codes are partial implementaitons of sample playlist and related classes. Preventing manipulations on the playlist item itself is omitted for simplicity.
Common solutions
There are many solutions to these type of situations, four most common of them are as follows;
2- Use internal methods or properties
3- Use cancellable pre-manipulation events
4- Hook into the public operations with a hooking interface
1 - Expose the playlist items as read-only collection and provide manipulation methods in the playlist class
You may expose the playlist items as a read-only collection and provide manipulation methods (add, remove...) in the playlist itself.
Sample implementation for read-only item collection and public manipulation methods is as follows;
public class PlaylistWithReadOnlyItems
{
List<PlaylistItem> m_Items;
public IList<PlaylistItem> Items { get; private set; }
public PlaylistWithReadOnlyItems()
{
m_Items = new List<PlaylistItem>();
//Expose read-only version of the list in order to prevent direct add-remove operations
this.Items = m_Items.AsReadOnly();
}
public void AddItem(PlaylistItem item)
{
m_Items.Add(item);
}
public void RemoveItem(PlaylistItem item)
{
m_Items.Remove(item);
}
//.....
}
The disadvantage of this pattern is that, using the Items
property for reading and public methods for manipulating the collection may be confusing for developers. Because developers are accustomed to use exposed collection porperties for both reading and manipulating its items.
Another way of exposing inner items for read-only purposes is providing an indexer which has only get
operator.
public PlaylistItem this[int index]
{
get
{
return m_Items[index];
}
}
2 - Use internal methods or properties
If you are developing a SDK or a component library that will be used by others, managing and protecting the internal state can be accomplished by using internal methods or properties.
The first step is to add internal control methods or properties to the class that will be under control. This sample uses internal properties in order to allow or prevent item operations on the custom collection class.
public class PlaylistWithInternalControlProperties : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
internal bool AllowAdd { get; set; }//Internal property for controlling "Add" operations
internal bool AllowRemove { get; set; }//Internal property for controlling "Remove" operations
//....
public void Add(PlaylistItem item)
{
if (this.AllowAdd)//Add the item if it is allowed
m_Items.Add(item);
else
{
//Maybe throw exception
}
}
public bool Remove(PlaylistItem item)
{
if (this.AllowRemove)//Remove the item if it is allowed
return m_Items.Remove(item);
return false;
}
//....
}
The next step is to control item operations from the owner object by settings the internal properties of the controlled object based on the current state. For example, in the below sample, adding and removing items are not allowed if the player is playing the list currently.
public class Player
{
PlaylistWithInternalControlProperties m_playlist;
PlayerState m_State;
public PlayerState State
{
get { return m_State; }
set
{
m_State = value;
//Allow or deny item operations on the playlist based on the current state of this player
m_playlist.AllowAdd = m_State != PlayerState.Playing;
m_playlist.AllowRemove = m_State != PlayerState.Playing;
}
}
//..
}
3 - Use cancellable pre-manipulation events
This pattern is very common in .Net Framework and 3rd party class libraries. This pattern makes use of events for notfying the owner about the changes that is going to be done on an object and give a chnace to cancel the operation.
The first step is to define operation types (mostly an enum definition) and an event args class that contains the information about the operation that is going to be done and has a Cancel
property. System.ComponentModel.CancelEventArgs
class can be used as the base class for the new event args class.
Sample operations enum and event args class are as follows;
public enum ItemOperation
{
Add,
Insert,
Remove,
Clear
}
//Event args class for holding the information about the item operation that is going to be done
public class PlaylistItemEventArgs : CancelEventArgs
{
//Related playlist item
public PlaylistItem Item { get; private set; }
//The operation that is goin to be done
public ItemOperation Operation { get; private set; }
public PlaylistItemEventArgs(PlaylistItem item, ItemOperation operation)
{
this.Item = item;
this.Operation = operation;
}
}
//Delegate for pre-manipulation event declarations
public delegate void PlaylistItemEventHandler(object sender, PlaylistItemEventArgs e);
Next step is to add pre-manipulation events (like ItemAdding
, ItemRemoving
, SomePropertyChanging
) and fire them before the operation is performed.
Third step is to cancel the operation if the Cancel
property of the event args class is set to true
after event processing is finished. There are two options for firing the pre-manipulation event and checking its Cancel
property.
The first option is to call all registered event handlers and check the Cancel
property at the end. OnItemAdding
method in the below code uses this technic. This technic is the most common one in .Net Framework class libraries. The danger of this technic is that, because the event is public and everyone can register to it, one of the methods in event handler chain may clear the Cancel
property to false
and cause the operation be done instead of being cancelled.
Second option is to call each handler, one by one and cancel the operation if one of them sets the Cancel
property to true. OnItemRemoving
method uses this technic.
public class PlaylistItemsCollectionWithCancellableEvents : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
//Pre-manipulation event, fired before adding an item
public event PlaylistItemEventHandler ItemAdding;
//Pre-manipulation event, fired before removing an item
public event PlaylistItemEventHandler ItemRemoving;
public PlaylistItemsCollectionWithCancellableEvents()
{
m_Items = new List<PlaylistItem>();
}
public void Add(PlaylistItem item)
{
//Prepare the manipulation operation information
PlaylistItemEventArgs _args = new PlaylistItemEventArgs(item, ItemOperation.Add);
//Notify the registered listeners about the operation and give chance to cancel it
OnItemAdding(_args);
if (_args.Cancel)//Cancel the operation if cancellation is requested
return;
m_Items.Add(item);//Add the item to the collection if the operation is not cancelled
//...
}
private void OnItemAdding(PlaylistItemEventArgs e)
{
if (ItemAdding != null)
ItemAdding(this, e);
}
public bool Remove(PlaylistItem item)
{
//Same as AddItem operation, but return true/false to inform the caller
//whether the operation was performed successfully or cancelled
PlaylistItemEventArgs _args = new PlaylistItemEventArgs(item, ItemOperation.Remove);
OnItemAdding(_args);
if (_args.Cancel)
return false;//Operation was not performed, item was not removed
m_Items.Remove(item);
//...
return true;//Item removed
}
private void OnItemRemoving(PlaylistItemEventArgs e)
{
Delegate[] _handlers = ItemRemoving.GetInvocationList();
foreach (PlaylistItemEventHandler handler in _handlers)
{
handler(this, e);
if (e.Cancel)
return;
}
}
//...
}
The last step is using these events to control item manipulations in the playlist item collection. The sample player class creates item collection and registers to its pre-manipulation events. When an event occures, checks its internal state and cancels the operation if the operation is not allowed at this state.
//In the player class
public class Player
{
//..
public Player()
{
m_playlist = new PlaylistItemsCollectionWithCancellableEvents();
//Register to pre-manipulation events
m_playlist.ItemAdding += new PlaylistItemEventHandler(m_playlist_ItemAdding);
m_playlist.ItemRemoving += new PlaylistItemEventHandler(_playlist_ItemRemoving);
//....
}
//...
private void m_playlist_ItemAdding(object sender, PlaylistItemEventArgs e)
{
//Does not allow add operation if the player state is "Playing"
if (this.State == PlayerState.Playing)
e.Cancel = true;
}
}
4 - Hook into the public operations with a hooking interface
This technic resembles the observable-observer pattern in Java which does not have simple event mechanism like in C#. Although this technic is very similar to using events, it may be faster and safer for some scenarios. One may also use this technic in order not to pollute the class interface with many pre-manipulation events.
The first step of this pattern is creating an interface for the operations that will be controlled. In this sample, we want to hook into AddItem
and RemoveItem
operations.
//Prepare a hooking interface for controlling the desired public operations
public interface IHookingInterface
{
bool CanAddItem(PlaylistItem item);
bool CanRemoveItem(PlaylistItem item);
//...
}
Second step is preparing the controlled class (PlaylistItem
collection in this case) which takes an object that implements the hooking interface. Controlled class (PlaylistWithHookablePublicOperations
) calls the appropriate methods of the hook interafece before it performs a public operation.
In the below example, m_Hook.CanAddItem
is called before performing the real add operation. If the call to m_Hook.CanAddItem
returns true
, the item is added. If it returns false
, it simply does nothing, but sometimes returning false
or throwing an exception may be required to tell to caller that the operation was not performed.
public class PlaylistWithHookablePublicOperations : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
IHookingInterface m_Hook;
//...
//Get the hook object as a constructor parameter,
//it is set by teh owner and not going to change
public PlaylistWithHookablePublicOperations(IHookingInterface hook)
{
m_Hook = hook;
m_Items = new List<PlaylistItem>();//Inner collection for actually holding the items
}
public void Add(PlaylistItem item)
{
//Ask to the hook object whether it is ok to add this item
if (m_Hook.CanAddItem(item))
m_Items.Add(item);//Add the item if it is allowed
else
{
//Maybe throw exception
}
}
public bool Remove(PlaylistItem item)
{
//Ask to the hook object whether it is ok to remove this item
if (m_Hook.CanRemoveItem(item))
return m_Items.Remove(item);//Remove the item if it is allowed
return false;//Item is not removed
}
//...
}
The next step is preparing the hook class that implements the hooking interface. These classes are mostly private classes defined in the owner classes. This way, it is possible for them to interact with the owner privately (can access private methods and state).
Do not implement this interface at the owner class itself, always create another small class. Implementing hooking interfaces at the owner classes will pollute their interfaces.
class InnerOrPublicHookClass : IHookingInterface
{
Player m_Owner;//The owner object whose state will be checked when deciding
//whether an operation can be performed
public InnerOrPublicHookClass(Player owner)
{
m_Owner = owner;
}
#region IHookingInterface Members
//Controls the item additions
public bool CanAddItem(PlaylistItem item)
{
//Do not allow the operation if the owner player is at "Playing" state
if (m_Owner.State == PlayerState.Playing)
return false;//Tell that operation should be cancelled
//... other controls if necessary
return true;//Tell that operation can be done
}
//Same as CanAddItem, but control the item removal
public bool CanRemoveItem(PlaylistItem item)
{
if (m_Owner.State == PlayerState.Playing)
return false;
//... other controls if necessary
return true;
}
#endregion
}
The last step is passing the hooking obkject to controlled object (the custom, controllable playlist item collection), mostly as a constructor parameter.
public class Player
{
PlaylistWithHookablePublicOperations m_Playlist;
public Player()
{
m_Playlist = new PlaylistWithHookablePublicOperations(new InnerOrPublicHookClass(this));
}
public PlayerState State { get; set; }
}
Although this technic requires a bit more code to write, it is more flexible and perfectly protects the controlled class from unwanted manipulations.
History
- 16 May 2017 - Initial version.
- 22 May 2017 - Read-only indexer option is added to the first method "Expose the playlist items as read-only collection and provide manipulation methods in the playlist class". Thanks to George Swan.