-
Notifications
You must be signed in to change notification settings - Fork 13
Completable objects
Certain types like the Bitmap class are "greedy" in a sense that they process all the presented data at once, even if the consumer only wanted a part of it. Bitmap parses the whole file before returning, and it doesn't care that you wanted only a part of it. This model doesn't help you read parts of bitmap, but it conceptualizes an idea of objects that aren't fully loaded, but may be queried for more data.
This concept was formed with two requirements in mind: to be easy to use and easy to implement. At the consumer side, accepting a completable object and retrieving its properties has to be as easy as if it were a normal object. At the producer side, implementing the completable interface should require minimal effort for the programmer, and almost no modifications to the parsing algorithm. If you already have a code that parses e.g. a file, it should be straightforward to convert the code to the completable pattern.
The ICompletable interface is the basis of this API. It represents an object whose state may be incomplete, and completed via the consumer's interactions. Completable properties of types that implement this interface shall be read-only, and defined using the Partial class. The generic Partial class represents a part of a completable object, which may or might not have been completed yet.
The standard mechanism is to take the Value of the properties, and the implementation of the Partial class (which actually inherits from Lazy) ensures that the property is fully loaded when you want to retrieve its value. The ICompletable interface also contains other mechanisms for values of its properties. RegisterReceiver registers an action for a property that is invoked when the value of the specified property is available. WaitForProperty is called internally when the value of a Partial property is requested, but it may be called directly.
"Waiting" for a property should invoke the code necessary to construct the value of the specified property. There is no actual waiting happening, or any other thread operation (this concept is intended for single-thread classes). If the property isn't assigned a value during the completion of the object, PropertyIncompleteException is thrown.
Assuming we have an object o with properties A and B, obtaining their value is straightforward:
Console.WriteLine(o.A.Value);
Console.WriteLine(o.B.Value);
As you can see from the example, the consumer doesn't have to use any methods from the ICompletable interface. The implementation ensures that no extra properties are constructed, and also that no code is run twice.
Please note that the actual algorithm that produces the properties is usually sequential. For an image, it means that usually the header comes before the bitmap data, so if you wait for the image size, the pixels will not be loaded. However, it is up to the actual class which properties come first.
If you have a sequential algorithm, the Completable class should be inherited from to make the implementation easier. It ensures that all methods are implemented correctly, and wraps the code that produces the properties into the completable pattern.
The constructor of a completable class should only store (and validate) its parameters and the rest of the code should be contained in the Completion method. The best way to implement the method is by using iterator blocks.
Assigning values to properties is done by yielding special "property assignment messages" which are constructed via the Property.Set method. Due to the usage of Lazy as the base type for the properties, the produced value is first added to a list, and retrieved when the value of the property is first needed.
The producer needs to initialize the properties in a special way, override the ContainsProperty method, and provide the implementation for the Completion method. The example type for the aforementioned type can be implemented like this:
public class CompletableTest : Completable
{
public Partial<int> A{get; private set;}
public Partial<int> B{get; private set;}
public CompletableTest()
{
A = new Partial<int>(this);
B = new Partial<int>(this);
A.MarkCreated();
B.MarkCreated();
}
public override bool ContainsProperty<T>(Partial<T> property)
{
if(Object.ReferenceEquals(property, A)) return true;
if(Object.ReferenceEquals(property, B)) return true;
else return false;
}
protected override IEnumerable<Property> Completion()
{
yield return Property.Set(A, 10);
Thread.Sleep(1000);
yield return Property.Set(B, 100);
}
}
If the consumer waits for property A, the value is returned immediately. If the consumer waits for property B, both properties are are initialized.
MarkCreated must be called exactly once in the constructor for all properties of the class. Other option is to use the constructor of Partial with the out Partial<T> property
parameter. The reason for this quirk is that the Partial class registers its own receiver, but this requires the object to already know that the property belongs to the instance (from ContainsProperty), so the properties must be assigned before that method is run. If you use readonly
fields to represent the properties, the alternative constructor can be called.
Because of the comparison of generic types, Object.ReferenceEquals must be used instead of ==
in the ContainsProperty method (the compiler probably thinks that the comparison will be never successful and doesn't realize that it can be so if the generic argument matched the type of the property).