4

A few years back, I wrote this answer to a question from which the following idea emerged.

I call these "Type Descriptors", as I have never been able to find this pattern documented or formally described anywhere - not to be confused with type descriptors in C#, but it's the most accurate name I've been able to come up with, so bear with me.

Here's my first attempt at a "formal" description:

A Type Descriptor is a type that provides meta-information about another type.

This pattern can be seen as an alternative to run-time reflection and annotations, and as such is equally applicable to languages that do or do not support reflection or annotations.

Type Descriptors are particularly useful as a means of describing aspects of model types found in for example domain and view-models, as well as in specialized types of models, such as form-models in a business- or web-application. Often such applications have many common aspects, such as the need to label inputs and perform common validations for e.g. required input, data types and formats, etc. - the information required by helper objects (or services) to handle these aspects fits comfortably in descriptors.

A descriptor mirrors the shape of the type it describes - for example, if a model type User has properties like id, name and email, it's type descriptor must have properties id, name and email, but where the properties on the model type have various value types (in this case for example integer and string) the properties of the descriptor are objects that provide information about those properties.

Maintaining identical model type and descriptor shapes may seem like duplication, in terms of property-names being identical in the model type and type descriptor - however, this does not violate the DRY principle as such, since the model type and descriptor, strictly speaking, are two different types that only happen to have the same shape, but do not have any overlapping concerns or duplication of any code; only the property names are duplicates.

I program mostly in PHP on a daily basis, and have used this pattern successfully to build applications. The following example is in PHP, though this could be implemented in any language capable of resolving properties by name at run-time.

A tiny bit of framework is required to implement this, so here goes...

A model-type implementing the following interface can provide a "type descriptor" - a type that describes the model-type itself:

interface TypeInfo
{
    function getType();
}

Individual model-properties are described by "property descriptors" in the "type descriptor", for which I use a base class along the lines of this:

abstract class PropertyInfo
{
    /** @var string */
    public $name;

    /** @var string */
    public $label;

    public function __construct($name)
    {
        $this->name = $name;
    }
}

From this class, I derive "property descriptors" for specific types of properties - for example, this type describes a string property:

class StringInfo extends PropertyInfo
{
    /**
     * @var int|null
     */
    public $max_length;
}

Now here's a sample User model, and it's "type descriptor" UserType:

class User implements TypeInfo
{
    /** @var string */
    public $name;

    /** @var string */
    public $email;

    /** @return UserType */
    public function getType()
    {
        return UserType::instance();
    }
}

class UserType
{
    /** @var StringInfo */
    public $name;

    /** @var StringInfo */
    public $email;

    public function __construct()
    {
        $this->name = new StringInfo('name');
        $this->name->label = 'Full Name';
        $this->name->max_length = 50;

        $this->email = new StringInfo('email');
        $this->email->label = 'E-mail Address';
        $this->email->max_length = 128;
    }

    /** @return self */
    public static function instance()
    {
        static $instance;

        return $instance ?: $instance = new static();
    }
}

As you can see, it's now possible to get the type descriptor either from a User instance by e.g. $user->getType() - notice that the property-names of the "property descriptors" in the "type descriptor" match the property-names in the actual model; using this pattern, it's my own responsibility to make sure the model and it's descriptor have the same shape.

Now I can write services and helpers etc. which may consume information from a "type descriptor" while working with model objects - for example, here's a very simple form helper:

class FormHelper
{
    /** @var TypeInfo */
    public $model;

    /** @var string */
    public $prefix;

    /**
     * @param TypeInfo $model
     * @param string     $prefix
     */
    public function __construct(TypeInfo $model, $prefix)
    {
        $this->model = $model;
        $this->prefix = $prefix;
    }

    /**
     * @param StringInfo $prop
     *
     * @return string
     */
    public function text(StringInfo $prop)
    {
        $name = $prop->name;

        return '<label for="' . $name . '">' . htmlspecialchars($prop->label) . '</label>'
            . '<input type="text" name="' . $this->prefix . '[' . $name . ']"'
            . ($prop->max_length ? ' maxlength="' . $prop->max_length . '"' : '')
            . ' value="' . $this->model->$name . '" id="' . $name . '" />';
    }
}

You would use the form helper like this:

$user = new User;
$user->name = 'Rasmus';
$user->email = '[email protected]';

$form = new FormHelper($user, 'user');

$t = $user->getType();

echo $form->text($t->name) . "\n";
echo $form->text($t->email) . "\n";

Which would output something like this:

<label for="name">Full Name</label><input type="text" name="user[name]" maxlength="50" value="Rasmus" id="name" />
<label for="email">E-mail Address</label><input type="text" name="user[email]" maxlength="128" value="[email protected]" id="email" />

The two key reasons I enjoy working like this are (1) patterns like rendering forms, mapping post-data to model objects, mapping model-objects to database records, performing input validation and so forth can all be performed by formal helpers and service objects, all of which consume the same single source of information, and (2) everything is "statically typed", meaning only a single unchecked property-reference (by name, as a string) is made and encapsulated inside the "type descriptor", and type-hints and inference in a modern IDE (PhpStorm) can provide automated code inspections at development-time, I can safely use automated refactorings, and so forth.

Okay, a rather lengthy discourse, for what boils down to one question: does this pattern have a name? :-)

Side question: does this constitute (reflective) meta-programming? (The use of this pattern is very similar to e.g. using reflection to obtain annotations from a model, though I find this to be much safer, less complicated, more flexible and transparent.)

9
  • Isn't GetType() just a Factory Method? Jul 15, 2014 at 22:12
  • Sounds like a combination of adding static typing (as runtime checks, rather than performed by the compiler) combined with annotation-based validation. If you've worked with statically-typed languages, how does it compare?
    – sea-rob
    Jul 15, 2014 at 22:14
  • 1
    @RobY: So, Statically-Typed Reflection, then? Jul 15, 2014 at 22:20
  • "Statically-typed Reflection", interesting - except, this isn't reflection, since there is no information derived from the type itself, not even the type-name is used... getType() isn't a factory method, I think the factory-method is UserType::instance()? As for how this compares to working with reflection/annotations in C# - I like this much better, it feels more like "programming" and less like "hacking", since you're leveraging ordinary OO language features and common OOP patterns; reflection/annotations always felt a bit like raping the source code to me ;-) Jul 16, 2014 at 8:37
  • Other terms I've used at times: "co-typing" or "meta-types". I like "type descriptor" best, it bugs me that it refers to a C# language feature, because it accurately describes what this other type is for. Do you know if "type descriptor" means anything in programming in general, or is it just something MS came up with for C#? Jul 16, 2014 at 8:41

1 Answer 1

1

That is an OOP Data Descriptor. Although you are describing additional information about a class rather than information about bits in memory, the fundamental parity still applies; you're describing in one place useful aspects about data in another place.

I'd advise you to adopt that name, write up a clearer definition of the pattern, and use the name in code. ("Type" already means something in PHP, so you really shouldn't use it.) So, your user class could be changed as follows:

class User {
   ...
   public function getDescriptor() {
   }
}

class UserDescriptor {
   ...
}
2
  • Very good, but I still don't see anything describing the idea of mirroring property-names 1:1 in the descriptor? Fundamentally I agree, this is a data-descriptor, but it's a little more than that - mirroring the property-names enables a kind of "cross reflection" going either from a property in a descriptor to an object property value, or vice-versa. I don't guess that makes it a new pattern though, just an extension to an existing pattern. Should I mark this as the correct answer? Aug 3, 2014 at 8:29
  • I received the following answer from Martin Fowler: "I’ve seen various things like it. The operational/knowledge layer distinction in my Analysis Pattern book is about this. Various people wrote up something like this under the name “Type Object” (an example is ksc.com/article3.htm, but I’m sure there’s more to dig out.) I remember something in Peter Coad’s work too. I bet there’s others, although I haven’t dug around in this territory for a while." - it looks like the "type object pattern" is more of an OOP generalization of the "data descriptor" pattern? Aug 3, 2014 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.