Try Pattern

Az előző cikkben elkészült EnumeratorFactory kódja nagyobb refaktort igényel, mert számos funkció zsúfolódott egyetlen metódusba. Nem fogunk a teljes átalakításon végigmenni, ennek a cikknek csupán az a célja, hogy bemutasson egy furcsa pattern-t. A pattern furcsasága abban rejlik, hogy látszólag ütközik más elvekkel. Nem is nagyon láttam még máshol alkalmazni, leszámítva a .NET néhány, ráadásul nem is kezdetektől fogva létező metódusát.

Nézzük a következő szelet kódot az előző cikkből:

foreach (var type in ...)        
{            
  var match = convention.Match(type.Name);             
  if (match.Success)            
  {                
    var formatType = match.Groups[1].Value;
    ...

Itt látunk három sort, ami arról szól, hogy ha illeszkedik a konvencióba egy név akkor történjen valami. A kód csúnyasága, hogy ezen a szinten csak az illeszkedés tényére/eredményére vagyunk kíváncsiak, mégis a Regex osztály dolgaival, az illeszkedés ellenőrzésének technikai részleteivel kell foglalkozni. Ez egy másik absztrakciós szint, ezért ki kell emelni legalább egy privát metódusba. Legtöbben ezt így tennénk:

string MatchConvention(string typeName)
{
  string result = null;

  var match = convention.Match(typeName);

  if (match.Success)
    result  = match.Groups[1].Value;

  return result;
}

Ezután a metódust így használnánk:

foreach (var type in ...)        
{            
  var formatType = MatchConvention(type.Name);
    
  if (formatType != null)
  {
    ...

Szép ez a kód? Nem tűnik csúnyának. De csak azért nem, mert nap mint nap így csináljuk és így szoktuk meg. Valójában a fenti kód nem annyira szép. Miért nem? Azért, mert a visszatérési érték egy esete fejezi ki azt, hogy a konvenció nem illeszkedik. Ez az eset most – és legtöbbször – null, mivel az úgysem ír le formátum típust, illetve a null jellemzően az érték hiányát jelöli.

A megoldás csúnyasága, hogy a metódus használójának tudnia kell a speciális helyzetről. A mi esetünkben ez nem probléma, mert ebben a pillanatban emeltük ki a kódot egy metódusba, tudjuk hogyan működik. De úgy általában, ez egy törékeny kódot okozó szokás, mivel nem erőlteti ki a használótól az ellenőrzést.

A szép megoldás az lenne, ha a metódushívás egy boolean értékkel mondaná meg, hogy sikerült-e az illeszkedés vagy nem. Ekkor valami ilyesmit írnánk le használat közben:

foreach (var type in ...)        
{                
  if (TryMatchConvention(type.Name))
  {
    ...

Itt sokkal kifejezőbb az if-es szerkezet. Egy gond van, és az elég nagy: nincs meg, hogy ha illeszkedik a típus neve a konvencióba, akkor a konvenció mit ad formátum típusnak.

Try pattern és a kimenő paraméter

Az eredmény visszaadására a try-pattern egy “out” módosítóval ellátott paramétert, tehát egy kimenő paramétert használ. A mi esetünkben ez így nézne ki:

foreach (var type in ...)        
{                
  string formatType;
  if (TryMatchConvention(type.Name, out formatType))
  {
    ...

Most bizonyára sokan gondolják azt, hogy ez sokkal rondább, mint a null visszatérési érték. De miért is? Egyéb indok azon kívül, hogy azt tanították, hogy a kimenő paraméter az rossz?

Miért rossz a kimenő paraméter?

Az out paraméter általában azért rossz, mert ha egy metódus egynél több dolgot akar közölni a hívójával, akkor vagy túl sok dolgot csinál, vagy azok a dolgok, amiket out-okban és visszatérési értékben visszaad, olyan szorosan összetartoznak, hogy esetleg saját osztály kellene, hogy reprezentálja őket. Nézzük például ezt a metódust:

public void GetPersonDetails(
		int id, 
		out string firstName, 
		out string lastName, 
		out DateTime birthDay);

Itt a három kimenő adat kerülhetne pl egy PersonDetails osztályba. Az out-ok használata jellemzően egy nevezetes Code Smell, mégpedig a Data Clumps smell jele.

A mi esetünk viszont nem ilyen. Az out paraméteres TryMatchConvention() esetében a boolean visszatérési érték és a formatType out paraméter csak pillanatnyi időre függnek össze – amíg meg nem történik a sikerességre vonatkozó ellenőrzés. Ezután a formatType vagy éli tovább a kódon belül az önálló életét, vagy az értékét figyelmen kívül hagyjuk, mivel a metódus visszatérési értéke jelezte, hogy a kimenő paraméter nem kapott (használható) értéket. Erre csinálni egy Data Clumps osztályt felesleges lenne.

Mit szól a Code Analysis?

Érdekes, hogy a visual studio-ba épített Code Analysis nem szereti a kimenő paramétereket. Ha ilyet lát, akkor a következő üzenetet kapjuk:

CA1021: Avoid out parameters

További érdekesség, hogy a Code Analysis tool nem küldi ezt az üzenetet, ha a metódus “Try”-jal kezdődik (lásd az MSDN oldal legalja) Emiatt például megbocsátó a .NET könyvtár TryParse() metódusaival szemben.

Pattern-e, ha nem híres?

A pattern lényege, hogy sokan használják, ismerik és egységesen beszélnek róla. Megvallom, én soha nem hallottam a try pattern-ről egészen tegnapig, amikor kerestem valami jó cikket annak alátámasztására, hogy nem csak szerintem nem bűn ebben az esetben az out paraméter. Más kódjában sem emlékszem, hogy láttam volna ezt a szerkezetet, bár lehet, hogy csak nem figyeltem fel rá. Az interneten egyetlen értelmes cikket találtam a témában, illetve a már említett MSDN oldalt. Van pár stack overflow kérdés az out paraméter legalitására vonatkozóan, ahol a válaszok között említik ezt a szerkezetet, mint elfogadható esete az out paramétereknek.

Szükség van rá egyáltalán?

Ha nem ismerik az emberek, felmerül a kérdés, hogy egyáltalán igény van-e erre. Nekem úgy tűnik, hogy inkább a rossz beidegződés miatt van az, hogy a programozók nem használják. Pedig elég gyakori helyzetről van szó. Nézzük például a refaktorált EnumeratorFactory kódját. A sötét sávok (devportálon nem látszik) jelzik a pattern használatát.

internal static class EnumeratorFactory
{
    static readonly string pattern = "^(?<format>.*)Enumerator$";
    static readonly Regex convention = new Regex(pattern, RegexOptions.Compiled);

    static Dictionary<string, Func<Stream, IEnumerator<Person>>> activators =
        new Dictionary<string, Func<Stream, IEnumerator<Person>>>(StringComparer.OrdinalIgnoreCase);

    static EnumeratorFactory()
    {
        ConfigureByConvention();
    } // EnumeratorFactory()

    internal static IEnumerator<Person> Create(string formatType, Stream source)
    {
        Func<Stream, IEnumerator<Person>> activator;
        if (activators.TryGetValue(formatType, out activator))
            return activator(source);

        throw new NotSupportedException("cannot create enumerator for " + formatType); 
    } // Create()

    private static void ConfigureByConvention()
    {
        foreach (var type in GetCandidateTypes())
        {
            CreateActivatorByConvention(type);
        } // foreach
    } // ConfigureByConvention()

    private static void CreateActivatorByConvention(Type type)
    {
        string formatType;
        if (TryMatchConvention(type.Name, out formatType))
        {
            Func<Stream, IEnumerator<Person>> activator;
            if (TryCreateActivator(type, out activator))
                activators[formatType] = activator;
        } // if
    } // CreateActivatorByConvention()        

    private static bool TryMatchConvention(string typeName, out string matchedFormatType)
    {
        var match = convention.Match(typeName);

        if (match.Success)
            matchedFormatType = match.Groups[1].Value;
        else
            matchedFormatType = null;

        return match.Success;
    } // TryMatchConvention()

    private static bool TryCreateActivator(Type type, out Func<Stream, IEnumerator<Person>> activator)
    {
        ConstructorInfo construtorInfo;
        if (TryGetConstructorInfo(type, out construtorInfo))
        {
            var ctorParam = Expression.Parameter(typeof(Stream));

            LambdaExpression activatorExpression =
                                Expression.Lambda(
                                    Expression.New(
                                        construtorInfo,
                                        new Expression[] { ctorParam }),
                                    ctorParam);
                
            activator = activatorExpression.Compile() as Func<Stream, IEnumerator<Person>>;
        }
        else
        {
            activator = null;
        } // else

        return activator != null;
    } // TryCreateActivator()

    private static bool TryGetConstructorInfo(Type type, out ConstructorInfo constructorInfo)
    {
        constructorInfo =  
            type.GetConstructor(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
                Type.DefaultBinder,
                new Type[] { typeof(Stream) },
                null);

        return constructorInfo != null;
    } // TryGetConstructorInfo()

    private static IEnumerable<Type> GetCandidateTypes()
    {
        return Assembly.GetExecutingAssembly().GetTypes();
    } // CandidateTypes()
} // class EnumeratorFactory
  1. #1 by tflamich on 2013. April 16. - 16:33

    Az igazán szép (valamint “sznob” és overkill) megoldás a Maybe monád alkalmazása lenne🙂
    http://ericlippert.com/2013/04/02/monads-part-twelve/

  2. #2 by zsschoner on 2013. April 16. - 18:55

    Köszi a cikket, jó a ‘try pattern’, én használom is esetleg még egy másfajta felhasználása az alábbi linken található TryGetValue.

    http://msdn.microsoft.com/en-us/library/dd287191.aspx

  3. #3 by Haruszame on 2013. June 3. - 16:13

    Nagyszerű cikk. Pont azért kezdtem el keresgélni, hogy erre a problémára valami alternatív megoldást keressek. Elsőre picit fura volt ez a megoldás, de nagyon tetszik. Azonnal előkerültek a progimban azok a pontok ahol elfelejtettem ellenőrizni.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: