Refaktoryzacja przy pomocy refleksji

Czasami zdarza się, że muszę przeprowadzić refaktoryzację, w której Resharper nie może mi pomóc. W moim ostatnim poście opisałem, jak przydatne mogą być wyrażenia regularne przy takiej pracy: Refaktoryzacj przy pomocy wyrażeń regularnych w Visual Studio

Tym razem sprawa jest inna i prosta podmiana nie zadziała w tym przypadku.

Domyślna wartość dla właściwości

Natrafiłem na kod, w którym atrybut DefaultValue był często używany, ale w nie tak, jak powinien. Atrybut DefaultValue jest częścią platformy .Net Framework i jest używany przez generatory kodu w celu sprawdzenia, czy wartość domyślna właściwości jest taka sama jak wartość bieżąca i czy należy wygenerować dla tej właściwości kod. Nie chcę wchodzić w szczegóły, ale możesz zajrzeć do tego artykułu, aby uzyskać więcej informacji.

W moim przypadku musiałem zmienić wszystkie domyślne wartości na  przypisania właściwości podczas tworzenia klasy. Problem polegał na tym, że ta klasa miała ponad 1000 takich właściwości, a zrobienie tego ręcznie nie tylko zajęłoby dużo czasu, ale mogłoby potencjalnie wprowadzić błędy.

Spójrzmy na przykładowy kod:

    public class DefaultSettings
    {
        public DefaultSettings()
        {
            // assignments should be here
        }

        [DefaultValue(3)]
        public int LoginNumber { get; set; }

        [DefaultValue("PrimeHotel")]
        public string HotelName { get; set; }

        [DefaultValue("London")]
        public string Town { get; set; }

        [DefaultValue("Greenwod")]
        public string Street { get; set; }

        [DefaultValue("3")]
        public string HouseNumber { get; set; }

        [DefaultValue(RoomType.Standard)]
        public RoomType Type { get; set; }
    }

Tutaj przydaje się refleksja. Mógłbym łatwo zidentyfikować atrybuty właściwości dla danego typu, ale jak zmienić je w kod?

Napiszmy test jednostkowy! Test jednostkowy to mały fragment kodu, który może korzystać z niemal każdej klasy z testowanego projektu i co najważniejsze – można go łatwo uruchomić. 💪

    [TestFixture]
    public class PropertyTest
    {
        [Test]
        public void Test1()
        {
            var prop = GetProperties();

            Assert.True(true);
        }

        public string GetProperties()
        {
            var sb = new StringBuilder();

            PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings));

            foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties)
            {
                var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)];

                if (attribute != null)
                {
                    // produce a string
                }
            }

            return sb.ToString();
        }
    }

To prosty kod, który uruchamia metodę GetProperties. Metoda ta pobiera PropertyDescriptorCollection która reprezentuje wszystkie właściwości w klasie DefaultSettings. Następnie dla każdej z nich sprawdzamy, czy zawiera DefaultValueAttribute. Teraz pozostaje tylko wygenerowanie kodu dla każdej właściwości przy użyciu klasy StringBuilder. Domyślasz się jak? Jest to łatwiejsze niż może się wydawać.

Sprawdźmy jak kod będzie się różnił dla poszczególnych typów:

public string GetProperties()
{
    var sb = new StringBuilder();

    PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings));

    foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties)
    {
        var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)];

        if (attribute != null)
        {
            if (sourceObjectProperty.PropertyType.IsEnum)
            {
                sb.AppendLine($"{sourceObjectProperty.Name} = {sourceObjectProperty.PropertyType.FullName.Replace("+", ".")}.{attribute.Value};");
            }
            else if (sourceObjectProperty.PropertyType.Name.Equals("string", StringComparison.OrdinalIgnoreCase))
            {
                sb.AppendLine($"{sourceObjectProperty.Name} = \"{attribute.Value}\";");
            }
            else
            {
                var value = attribute.Value == null ? "null" : attribute.Value;
                sb.AppendLine($"{sourceObjectProperty.Name} = {value};");
            }
        }
    }

    return sb.ToString();
}

Dla typu enum, musimy wyświetlić przestrzeń nazw(namespace) oraz typ i jego wartość po kropce. Dla ciągów znaków,  musimy wstawić apostrofy, a dla typu nullowalnego, wartością powinno być null. Dla innych musimy tylko przypisać wartość.

Jesteś ciekawy co z tego wyszło? 🤔

Powiedziałbym, że całkiem, całkiem:) 

Kiedy wkleję go do mojego konstruktora, kod będzie wyglądał następująco:

    public DefaultSettings()
    {
        LoginNumber = 3;
        HotelName = "PrimeHotel";
        Town = "London";
        Street = "Greenwod";
        HouseNumber = "3";
        Type = PrimeHotel.Web.Models.RoomType.Standard;
    }

I wiesz co? Działa!

Podsumowanie

Używanie refleksji nie jest najlepszą praktyką, ale jest ona bardzo potężnym narzędziem. W tym przykładzie pokazałem, jak jej użyć i wywołać ten kod w bardzo prosty sposób – jako test jednostkowy. Nie jest to kod, który bym commitował, ale w przypadku podejścia prób i błędów jest świetny. Jeśli natrafiłeś na takie ręczne zadanie w swoim kodzie, spróbuj je zautomatyzować 😎

Mam nadzieję, że nauczyłeś się czegoś dzisiaj i jeżeli tak, to świetnie! 🍺

Nie zapomnij zapisać się na newsletter, aby nie przegapić kolejnych postów. 📣

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *