Generic Mapper های نوشته شده به روش TDD

پنجشنبه 14 مرداد 1395

این مقاله به شما آموزش می دهد که چگونه به آسانی یک generic mapper به روش TDD بنویسید. generic mapper هایی که به این روش می سازید می توانند در پروژه های بعدی مورد استفاده قرار بگیرند.

Generic Mapper  های نوشته شده به روش TDD

Mapper چیست و چرا باید از آن استفاده کنیم؟

Mapper ها برای نگارش مجدد یک شی بر روی شی دیگر استفاده می شوند. اغلب این شی ها ، شی هایی از جنس مشابه می باشند و فقط در لایه های مختلفی از برنامه قرار دارند.

در برنامه های ساده و کوچک اغلب به استفاده از Mapper ها نیازی نیست و نگارش مجدد داده ها  از یک شی به شی دیگر به صورت دستی انجام می شود، به این خاطر که این اشیاءمعمولا کاملا متفاوت هستند. به عنوان مثال:

var email = new Email
{
    From = user.Name,
    Topic = topicTxt.Text,
    Body = bodyTxt.Text,
    Date = DateTime.Now
};

اما در برنامه های بزرگ تر و پیچیده تر، کلاس های زیادی وجود دارند که موجودیت های برنامه را نمایش می دهند. این مورد در زمان هایی که ترکیبی از Data Access Layer  ، Data Transaction Objects و یا ViewModel ها را داریم، به خوبی می تواند دیده شود. برنامه هایی که به سه لایه تقسیم می شوند به صورت زیر خواهند بود:

var customerDto = new CustomerDto
{
    Id = customerEntity.Id,
    Name = customerEntity.Name,
    Surname = customerEntity.Surname
};

var customerViewModel = new CustomerViewModel
{
    Id = customerDto.Id,
    Name = customerDto.Name,
    Surname = customerDto.Surname
};

البته در این مثال از constructor ها نیز برای پذیرش شی ها و متد ها استفاده شده است. اما در کد همچنان مقدار دهی های اولیه به متغیرها را نیز شاهد هستیم.

یک mapper می تواند این کار را به شیوه ای جادویی برای شما انجام بدهد.


var customerDto = mapper.Map<CustomerDto>(customerEntity);

var customerViewModel = mapper.Map<CustomerViewModel>(customerDto);

TDD  مخفف عبارت Test Driven Development (روش توسعه برنامه ها مبتنی بر تست) است. ایده این کار ، این است که ابتدا یک Unit Test می نویسیم ، سپس پیاده سازی را متناسب با تست انجام می دهیم.

نوشتن تست ها مزایای زیادی دارد. یکی از مزایا این است که کد را نسبت به تغییرات احتمالی، پایدار و مقاوم می کند. (به کد قابلیت انعطاف پذیری می بخشد). همچنین زمان لازم برای تست پروژه را نیز کاهش می دهد، به خصوص زمانی که مسیرهایی که باید مورد تست قرار بگیرند، زیاد هستند و یا تست برنامه، زمان زیادی نیاز دارد.

TDD یک مزیت بزرگ دیگر نیز  دارد. برخی اوقات می بینیم خروجی کار ما ، ساده است ولی پیاده سازی آن پیچیده است و در نگاه اول، کدنویسی ، دشوار به نظر می آید.

مزیت دیگر نیز این است که اساسا ما زمانی که با API ها کار می کنیم، این Unit Test ها می توانند بسیار کاربردی و مفید باشند. این جمله به این معنی است که زمانی که شروع به نوشتن تست ها می کنیم، در حقیقت نحوه ی کار API ها را می دانیم. ابزار زیادی وجود دارند که می توانند همین کار را برای ما انجام بدهند ولی استفاده از آن ها چندان هم راحت نیست.

استفاده از کد

برای افرادی که با TDD و یا حتی Unit Test کار نکرده اند، کار ما با ساخت یک پروژه شروع می شود.

در Visual Studio ، گزینه File -> New... را انتخاب کنید.

در پنجره  New Project به دنبال پروژه ی Unit Test بگردید (می توانید آن را در Templates -> Visual C# -> Test پیدا کنید. ) نام مورد نظرتان را برای پروژه وارد کنید و پروژه را ایجاد کنید.

حالا یک کلاس جدید به نام Unit-Tests ایجاد کنید.

بر روی نام پروژه کلیک راست کنید و گزینه ی  Add -> Unit Test... را انتخاب کنید.

سپس نام فایل را به GenericMapperTests.cs تغییر بدهید.

Visual Studio به صورت خودکار همه ی reference های ضروری برای نوشتن Unit Test ها را فراهم می کند. بنابراین بیایید اولین Unit Test را بنویسیم.

[TestClass]
public class GenericMapperTests
{
    [TestMethod]
    public void ShouldMapPropertiesFromOneObjectToAnother()
    {
        // Given

        // When

        // Then
    }
}

کاری که ما می خواهیم انجام دهیم، ساخت و پر کردن یک شی است. سپس می خواهیم یک شی مشابه ایجاد کنیم و مقادیر را از شی اول به دومی بازنویسی کنیم.

تست کامل مطابق زیر خواهد بود:

[TestMethod]
public void ShouldMapPropertiesFromOneObjectToAnother()
{
    // Given
    var customer = new Customer
    {
        Id = 1,
        Name = "Miłosz",
        Surname = "Wieczorek"
    };
    var newCustomer = new Customer();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, newCustomer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

ما قبل از این کار در کلاس GenericMapper ، شی ای به نام Customer ایجاد کرده ایم.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

public class GenericMapper
{
    public void Map(Customer customer, Customer newCustomer)
    {
    }
}

همان طور که در عنوان این مقاله هم گفته شده است، این کلاس، باید نقش یک Generic Mapper را ایفا کند. بنابراین لازم است این متد را به گونه ای تغییر بدهیم که بتواند این کار را انجام دهد.

public class GenericMapper
    {
        public void Map<T>(T from, T to)
        {
            var type = typeof(T);
            var properties = type.GetProperties();
            foreach (var property in properties)
            {
                property.SetValue(to, property.GetValue(from));
            }
        }
    }

حالا Mapper ،می تواند Propety ها را از یک شی به یک شی دیگر نگاشت بدهد، اما برای این کار لازم است شی ها از یک نوع باشند. ما می توانیم Mapper خودمان را ارتقا بدهیم به طوری که بتواند شی ها را از یک نوع به نوع دیگری نیز نگاشت کند.

نوشتن بقیه کدهای مربوط به Mapper نیز ساده است .

[TestMethod]
public void ShouldMapPropertiesFromOneObjectToAnotherWithDifferentTypes()
{	
    // Given
    var customer = new Customer
    {
        Id = 1,
        Name = "Miłosz",
        Surname = "Wieczorek"
    };
    var newCustomer = new CustomerDto();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, newCustomer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

به هر حال ما شی جدید خود را نیز ایجاد کرده ایم که کاملا مشابه Customer است.

public class CustomerDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

متاسفانه این تغییرات باعث ایجاد خطا در زمان کامپایل خواهند شد به طوری که متد Map در Mapper به ما اجازه نمی دهد دو تا نوع مختلف را پاس بدهیم.

متد Map را کمی ارتقا می دهیم تا بتواند دو تست را انجام بدهد.

public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var propertiesFrom = typeFrom.GetProperties();
    var propertiesTo = typeTo.GetProperties();

    foreach (var propFrom in propertiesFrom)
    {
        foreach (var propTo in propertiesTo)
        {
            if (propTo.Name == propFrom.Name &&
                propTo.PropertyType == propFrom.PropertyType)
            {
                propTo.SetValue(to, propFrom.GetValue(from));
            }
        }
    }
}

حالا تست را اجرا کنید. خواهید دید که همه ی تست ها با موفقیت اجرا خواهند شد.

اما این کد ها چندان هم مناسب نیستند... برای بهتر کردن این کدها می توانیم از LINQ استفاده کنیم.

public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var properties = typeFrom.GetProperties()
        .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
        {
            propFrom = f,
            propTo = t
        });

    foreach (var prop in properties.Where(p => p.propFrom.PropertyType == p.propTo.PropertyType))
    {
        prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
    }
}

حالا ساختار کد کمی بهتر شده است.

Mapper اصلی به درستی کار می کند. حالا بیایید ویژگی هایی که Mapper باید داشته باشد را بررسی کنیم.

یکی از ویژگی هایی که می خواهیم Mapper ما داشته باشد، این است که نتواند بر روی فیلد های فقط خواندنی، بنویسد . ما همچنین می خواهیم در طرف مقابل نیز همچنین موردی را داشته باشیم: یعنی یک مقدار نباید بتواند از فیلد های فقط نوشتنی ، قابل دوباره نویسی باشد.

تکه کد های مربوط به این بخش را می نویسیم:

[TestMethod]
public void ShouldNotMapReadonlyAndWriteOnlyFields()
{
    // Given
    var customer = new Customer
    {
        Id = 1,
        DateOfBirth = new DateTime(1990, 01, 01),
        Age = 26,
        UpdatedBy = 15
    };

    var customerDto = new CustomerDto();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, customerDto);

    // Then
    Assert.AreEqual(customer.DateOfBirth, customerDto.DateOfBirth, "Date of birth");
    Assert.AreNotEqual(customer.Age, customerDto.Age, "Age");
    Assert.AreEqual(default(int), customerDto.UpdatedBy, "UpdatedBy");
    Assert.AreNotEqual(customer.GetUpdatedBy(), customerDto.UpdatedBy, "Age");
}

در زیر شی های به روز رسانی شده ی Customer  و  CustomerDto را می توانیم مشاهده کنیم.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }
    public int Age { get; set; }

    public int UpdatedBy
    {
        private get;
        set;
    }

    public int GetUpdatedBy()
    {
        return UpdatedBy;
    }
}

public class CustomerDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }

    public int Age
    {
        get;
        private set;
    }

    public int UpdatedBy { get; set; }
}

کمی نیز در بخش تست، تغییر ایجاد می کنیم .

public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var properties = typeFrom.GetProperties()
        .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
        {
            propFrom = f,
            propTo = t
        });

    foreach (var prop in properties.Where(p => p.propFrom.PropertyType == p.propTo.PropertyType && p.propFrom.CanRead && p.propTo.CanWrite && p.propFrom.GetMethod.IsPublic && p.propTo.SetMethod.IsPublic))
    {
        prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
    }
}

 تست بهتر شده است، فقط کمی به بخش بندی نیاز دارد.

public class GenericMapper
{
    public void Map<TFrom, TResult>(TFrom from, TResult to)
    {
        var typeFrom = typeof(TFrom);
        var typeTo = typeof(TResult);
        var properties = typeFrom.GetProperties()
            .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
            {
                propFrom = f,
                propTo = t
            });

        foreach (var prop in properties.Where(p => CanRewriteValue(p.propFrom, p.propTo)))
        {
            prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
        }
    }

    private bool CanRewriteValue(PropertyInfo propFrom, PropertyInfo propTo)
    {
        return propFrom.PropertyType == propTo.PropertyType &&
            propFrom.CanRead &&
            propTo.CanWrite &&
            propFrom.GetMethod.IsPublic &&
            propTo.SetMethod.IsPublic;
    }
}

 برنامه بسیار بهتر شد.

حالا یک Generic Mapper کارآمد و خوب ایجاد کرده ایم. اما همان طور که در Unit Test ها نیز می توانیم ببینیم، استفاده از Mapper در حال حاضر ، کمی دشوار است.

در ابتدا ، Mapper  را استاتیک می کنیم. این کار، تست هایی که تا به حال ایجاد کرده ایم را تحت تاثیر قرار می دهد اما تغییر بزرگی به حساب نمی آید.

علاوه بر این ، ما می خواهیم Mapper ما یک نمونه جدید از شی همراه با Property های نگاشت شده، ایجاد کند . برای این کار، نیاز داریم تا یک تست جدید بنویسیم.

اضافه کردن این ویژگی به Mapper ، آن را بسیار کاربردی می کند.

حالا ایجاد یک شی را بررسی می کنیم.

[TestMethod]
public void ShouldCreateNewObjectWithAlreadyMappedProperties()
{
    // Given
    var customer = new Customer
    {
        Id = 4,
        Name = "John",
        Surname = "Doe"
    };

    // When
    var newCustomer = GenericMapper.Create<Customer, CustomerDto>(customer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

پیاده سازی آن نیز بسیار ساده است، ولی در نهایت کارایی برنامه را به طرز چشمگیری افزایش می دهد.

public static TResult Create<TFrom, TResult>(TFrom source) where TResult : class, new()
{
    var result = new TResult();
    Map(source, result);
    return result;
} 

کاری که می خواستیم ، انجام شد.

این Generic Mapper ساده به همراه Unit Test هایی که ساختیم، می توانند به سادگی به پروژه های دیگر منتقل شوند و متناسب با نیاز آن پروژه، تغییر کنند و مورد استفاده قرار بگیرند.

همچنین Generic Mappe می تواند با اضافه کردن تعدادی از قابلیت های دیگر، به سادگی توسعه داده شود. برای این کار کافی است تست مورد نظر نوشته شود و موارد مورد نیاز برای پیاده سازی آن فراهم شود.

آموزش سی شارپ

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان
  • C#.net
  • 1k بازدید
  • 3 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید