(MVC) MVC (2016 год)

Застосування патерну Dependency Injection за допомогою IoC-контейнера Ninject

Існує багато можливостей зробити просту програму складною. Один із самих поширених засобів замінити одну срічку кода на тисячу стрічок - це застосовання IoC-контейнеров. Зараз я покажу вас фокус, як завдання на ініціалізацію бази можна виконати просто - без Ninject (за допомогою буквально декількох стрічок кода та декількох кліков мишкою), а потім покажу альтернативний шаблон - за допомогою EntityFramework та Ninject. А якщо в мене вистачить часу - то потім покажу як можна це просте завдання розвести ще втричі більше, за допомогою так званого TDD (test driven development).

Глобальна мета патерну Dependency Injection - це спланувати спочатку всі важливі інтерфейси системи, завантажити їх у Ninject, а потім робити саму систему, посилаючись у кожному компоненті на заздалегідь написані інтерфейси. Тобто цей патерн - це засіб розділити роботу між багатьма програмістами та зробити незалежними роботу одних програмістів від інших (якщо всі дотримуються запланованих інтерфейсів, то всі мають можливість працювати одночасно). Інша мета цього патерну - ніхто з окремих груп програмістів не розуміє всю задумку цілком. Тобто кожний робить маленький фрагмент по заданим інтерфейсам, до яких він звертається з одного боку, а з іншого боку до його програм хтось звертається незрозумілодля яких цілій взагалі.

Такий підхід дозволяє зберігати власність на систему у однієї людини, тієї що спочатку спланувала всі інтерфейси. Всі останні люди - просто гвинтики, які не розуміють чим вони займаються взагалі. У них є щоденний план на кількість кода та зарплата. Це дуже цікавий режим для власників бізнесу і максимально некомфортний режим для програмістів. У плані накладних витрат на програмування застосування цього патерну приводить, наприклад, до 10-ти кратного збільшення коду, який потрібно написати. Ось, загально кажучи, що означає патерн Dependency Injection.

1. Виконуємо задачку найпростішим засобом.

Утворюємо проєкт і робимо п'ять кліков мишкою, щоб підготувати до використання Linq-to-SQL.



Потім беремо ось такий заздалегідь підготовлений файлік.



І ось таким кодом завантажуємо його в базу.



Як бачите, кількість коду точно відповідає нашій задачці. Тобто 4-5 смислових стрічок коду і декілька формальних об'яв, які студія додала сама end sub, end module. end function, end using, module Module1, Sub Main() ...


   1:  Module Module1
   2:   
   3:      Sub Main()
   4:          Dim db1 As New TheHouseDBDataContext
   5:          For Each One In LoadBrandJson()
   6:              Dim X = New AutoBrand With {.AutoBrandName = One("AutoBrandName")}
   7:              db1.AutoBrands.InsertOnSubmit(X)
   8:              db1.SubmitChanges()
   9:          Next
  10:          System.Console.WriteLine("DB initialize succesfully")
  11:          Console.ReadKey()
  12:      End Sub
  13:   
  14:      Private Function LoadBrandJson() As IEnumerable
  15:          Using streamReader As New IO.StreamReader("AutoBrand.json")
  16:              Dim Json As String = streamReader.ReadToEnd()
  17:              Return Newtonsoft.Json.JsonConvert.DeserializeObject(Json)
  18:          End Using
  19:      End Function
  20:   
  21:  End Module

2. Вибираємо найскладніший та найповільніший засіб.

Але зараз я вас здивую - чи можна цю ж саму задачку виконати у мільйон раз повільніше та у тисячі разів більшим кодом? Так, це можливо! Для цього потрібно застосувати патерн Dependency Injection!

Але нам ще потрібно вибрати самий найповільніший IoC container! Для цього уважно роздивляємось ось цю табличку (якщо цей сайт вже не буде існувати, подивиться на локальну копію цієї сторінки).

Чудово! Вихід знайдено - будемо використовувати Ninject.

3. Проєкт #1 - застосуємо мапер EntityFramework.

Спочатку застосуємо мапер EntityFramework - це чудова задумка зробити софт найповільнішим з можливих. Ця нова чудо-бібліотека від Мікрософта не тільки не підтримує View SQL-серверу, але навіть не вміє зробити NOLOCK при запросах у базу! Вона взагалі використовую MS SQL у якомусь режимі MySQL, тобто ігнорую 99,99% функціоналу MS SQL. Для цього мапера - MS SQL це лише засіб зберігати таблички. Нібито нічого іншого у MS SQL не існує. Дуже точний аналог цього використовування MS SQL - це використання мікроскопу для забивання гвіздочків. Точно так використвую EntityFramework можливості MS SQL.



Цей мапер EntityFramework має якісь можливості додаткові, порівняно з Linq-to-SQL - наприклад дозволяє по-різному зв'язати імена таблиць у MS SQL з іменами полей у моделі. Та хиба VIEW будь-якого SQL Server'у не дозволяє зробити теж саме? Чи ви не можете вибрати будь-які імена у процедурах? Я так і не зрозумів, навіщо цей функціонал потрібно було виносити з рівня MS SQL на рівень вище - у студію. Чи може MS планую вбити MS SQL та купити MySQL? Так і у MySQL вже існують вьюхі та процедури. Навіщо потрібен дубль вже існуючуго функціоналу - мені незрозуміло. Також це стосується і планування моделей. Невже Database diagramm, які існують у MS SQL - чомусь гірше, ніж візуальний дизайнер EntityFramework чи такого ж дизайнера Linq-to-SQL? Хм, запитання є, але відповіді я поки що не бачу, можливо істина відкриється пізніше, наприклад, коли Мікрософт продасть MS SQL сторонній компанії та примусить усіх перейти на якийсь "зберігач табличок" замість MS SQL. Може тоді буде зрозуміло призначення EntityFramework.

4. Проєкт #2 - General Repository.

Робимо наступний рівень софта - загально репозіторії-патерну. Головне питання - навіщо взагалі цей рівень потрібен? Звиняйте, друзі - а що SQL-команди Insert, Select, Delete - це не є репозіторі-патерн? Особливо коли вони виконуються у транзакціях з багатьма табличками?

До того ж, головна бібліотека доступу к даним Linq-to-SQL вже має дубль названих команд рівня SQL, тобто той же самий репозіторі-патерн дублюється рівнем вище - методами Linq-to-SQL такими як Select, DeleteOnSubmit, InsertOnSubmit. Навіщо потрібно самому робити у явному вигляді цей рівень, до того ж він існує у багатьох варіантах софта. Моє припущення те ж саме - Мікрософт готує MS SQL до продажу стороньої компанії і бажає відокремити прикладний софт від MS SQL.

Я вам пропоную прочитати про цей рівень софта додатково ці статті:

І я зроблю на цьому рівні дуже специфічний метод, який вміє працювати з колекціями:



Цей код цікавий у декількох напрямках. По-перше, він буде зовсім різний залежно від версії EntityFramework. Тобто, у 2010-студії одна й та ж сама версія EntityFramework має ObjectContext, а якщо зробити все те ж саме у більш сучасній версії студії, то буде вже dbContext. Будь ласка, почитайте про цю проблему ось тут:

Тобто у старій студії з ObjectContext цей код буде таким:


   1:  Imports DJ.EF_definition
   2:  Imports System.Data.Entity
   3:   
   4:  Public Class Repository(Of TEntity As Class)
   5:      Implements IRepository(Of TEntity)
   6:   
   7:      Friend context As EF_definition.TheHouseEntities1
   8:      Friend dbSet As System.Data.Objects.ObjectSet(Of TEntity)
   9:   
  10:      Public Sub New(context As EF_definition.TheHouseEntities1)
  11:          Me.context = context
  12:          Me.dbSet = context.CreateObjectSet(Of TEntity)()
  13:      End Sub
  14:   
  15:      Public Sub InsertCollection(entityCollection As System.Collections.Generic.List(Of TEntity)) _
  16:          Implements IRepository(Of TEntity).InsertCollection
  17:          Try
  18:              entityCollection.ForEach(Sub(e)
  19:                                           dbSet.AddObject(e)
  20:                                       End Sub)
  21:              context.SaveChanges()
  22:   
  23:          Catch ex As Entity.Validation.DbEntityValidationException
  24:              Dim sb As New Text.StringBuilder()
  25:   
  26:              For Each failure In ex.EntityValidationErrors
  27:                  sb.AppendFormat("{0} failed validation" & vbLf, failure.Entry.Entity.[GetType]())
  28:                  For Each [error] In failure.ValidationErrors
  29:                      sb.AppendFormat("- {0} : {1}", [error].PropertyName, [error].ErrorMessage)
  30:                      sb.AppendLine()
  31:                  Next
  32:              Next
  33:   
  34:              Throw New Entity.Validation.DbEntityValidationException("Entity Validation Failed - errors follow:" & vbLf + sb.ToString(), ex)
  35:          End Try
  36:      End Sub
  37:   
  38:      Public Sub Dispose1() Implements IRepository(Of TEntity).Dispose
  39:          GC.SuppressFinalize(Me)
  40:      End Sub
  41:   
  42:  End Class

А у новій студії ось таким:


...
   8:      Friend dbSet As Entity.DbSet(Of TEntity)
...
  12:          Me.dbSet = context.Set(Of TEntity)()
...
  19:                                           dbSet.Add(e)
...

Зверніть увагу на головну стрічку 19 цього коду, все останнє - тут фактично wrapper навколо цієї найбільш важливої стрічки коду. При чому це не просто код, а це LAMBDA Expression.

І останній цікавий момент цього коду - оскільки майже всі методи EntityFramework - це методи Extension - то без Import у першої стрічці кода нічого працювати не буде, саме Import дозволяє студії вичитати Extension-функції.

4. Проєкт #3 - рівень сервісів.

Тепер утворюємо рівень сервісів, це той самий рівень, який ми віддамо Ninject'у, тобто з цім рівнем софта буде через Ninject спілкуватися сторонній софт. Зрозуміло, що у реальному світі ми віддамо Ninject'у лише інтерфейси, а не класи, що реалізують інтерфейси.



   1:  Interface ITheHouseService
   2:      Inherits IDisposable
   3:   
   4:      Sub InsertBrand(DataList As List(Of DJ.EF_definition.AutoBrand))
   5:      Sub InsertCountry(DataList As List(Of DJ.EF_definition.Country))
   6:   
   7:  End Interface


   1:  Public Class InsertService
   2:      Implements ITheHouseService
   3:   
   4:      Private ReadOnly RepoBrand As DJ.Repo.IRepository(Of DJ.EF_definition.AutoBrand)
   5:      Private ReadOnly RepoCountry As DJ.Repo.IRepository(Of DJ.EF_definition.Country)
   6:   
   7:      Public Sub New(BrandData As DJ.Repo.IRepository(Of DJ.EF_definition.AutoBrand),
   8:                     CountryData As DJ.Repo.IRepository(Of DJ.EF_definition.Country))
   9:          Me.RepoBrand = BrandData
  10:          Me.RepoCountry = CountryData
  11:      End Sub
  12:   
  13:      Public Sub InsertBrand(DataList As System.Collections.Generic.List(Of EF_definition.AutoBrand)) _
  14:                      Implements ITheHouseService.InsertBrand
  15:          RepoBrand.InsertCollection(DataList)
  16:      End Sub
  17:   
  18:      Public Sub InsertCountry(DataList As System.Collections.Generic.List(Of EF_definition.Country)) _
  19:                      Implements ITheHouseService.InsertCountry
  20:          RepoCountry.InsertCollection(DataList)
  21:      End Sub
  22:   
  23:  #Region "IDisposable Support"
  24:      Private disposedValue As Boolean ' To detect redundant calls
  25:   
  26:      ' IDisposable
  27:      Protected Overridable Sub Dispose(disposing As Boolean)
  28:          If Not Me.disposedValue Then
  29:              If disposing Then
  30:                  ' TODO: dispose managed state (managed objects).
  31:                  RepoBrand.Dispose()
  32:                  RepoCountry.Dispose()
  33:              End If
  34:   
  35:              ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
  36:              ' TODO: set large fields to null.
  37:          End If
  38:          Me.disposedValue = True
  39:      End Sub
  40:   
  41:      ' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
  42:      'Protected Overrides Sub Finalize()
  43:      '    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
  44:      '    Dispose(False)
  45:      '    MyBase.Finalize()
  46:      'End Sub
  47:   
  48:      ' This code added by Visual Basic to correctly implement the disposable pattern.
  49:      Public Sub Dispose() Implements IDisposable.Dispose
  50:          ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
  51:          Dispose(True)
  52:          GC.SuppressFinalize(Me)
  53:      End Sub
  54:  #End Region
  55:   
  56:  End Class

Цей код не має якихось цікавостей, взагалі тут самого кода одна чи дві стрічки - п'ятнадцята та двадцята, все останнє - лише wrapper (обв'язка) навколо цього смислового кода, який виконує звернення до рівня нище, до кода який ми зробили у попередньому проєкті. Едина цікавість - це використовування патерна Dispose. Навіщо це робити - мені не зовсім зрозуміло, сборка мусора у .NET чудово працює і без цього патерна. Навіщо це робити самому власними ручками? Але все так чомусь роблять...

4. Проєкт #4 - завантажуємо всі інтерфейси у Ninject і користуємось ними.



Цей код цікавий, все що було зроблено раніше, було зроблено - саме як підготовка до цього коду. Подивимося на нього детальніше. По-перше нам потрібно завантажити всі іттерфейси у Ninject.


Нажаль у цьому коді нище є помилка, яка зводить використання всього патерну майже до нуля - нам тепер буде потрібно перераховувати всі інтерфейси бази. Це дуже прикро, без їх перерахування ручками, якщо вони б підтягнулися самі - було б набагато краще. Яко хтось знає відгадку синтаксичної конструкції VB.NET, будь ласка напишіть її в каментах.


   1:  Imports Ninject
   2:  Imports DJ.EF_definition
   3:   
   4:  Public Class ExportModule
   5:      Inherits Ninject.Modules.NinjectModule
   6:   
   7:      Public Overrides Sub Load()
   8:          Bind(Type.[GetType]("DJ.EF_definition.TheHouseEntities1, DJ.EF-definition")).ToSelf().InSingletonScope()
   9:   
  10:          'Це зроблено неправильно, в Шарпе та ж сама думка записується 
  11:          'без конкретного типу, незрозуміло як це записати в сінтаксичних конструкціях бейсіка, без задання конкретного типу це неприпустимий сінтаксіс VB.NET
  12:          'Bind(typeof(IRepository<>)).To(typeof(Repository<>)).InSingletonScope();
  13:   
  14:          Bind(GetType(DJ.Repo.IRepository(Of DJ.EF_definition.AutoBrand))).[To](GetType(DJ.Repo.Repository(Of DJ.EF_definition.AutoBrand))).InSingletonScope()
  15:          Bind(GetType(DJ.Repo.IRepository(Of DJ.EF_definition.Country))).[To](GetType(DJ.Repo.Repository(Of DJ.EF_definition.Country))).InSingletonScope()
  16:      End Sub
  17:   
  18:  End Class

Далі зугружаємо у Ninject всі операції:


   1:  Imports Ninject
   2:   
   3:  Public Class ExportOperation
   4:      'реализацію классу InsertService не бачить клиент, він знае тільки про інтерфейс ITheHouseService
   5:      Private ReadOnly InsService As DJ.Service.InsertService
   6:   
   7:      Public Sub New(kernel As Ninject.IKernel)
   8:          InsService = kernel.Get(Of DJ.Service.InsertService)()
   9:      End Sub
  10:   
  11:      Public Sub SaveData()
  12:          InsService.InsertBrand(LoadBrandJson())
  13:      End Sub
  14:   
  15:      Private Function LoadBrandJson() As List(Of DJ.EF_definition.AutoBrand)
  16:          Using streamReader As New IO.StreamReader("AutoBrand.json")
  17:              Dim Json As String = streamReader.ReadToEnd()
  18:              Dim AutoBrand As List(Of DJ.EF_definition.AutoBrand) = Newtonsoft.Json.JsonConvert.DeserializeObject(Of List(Of DJ.EF_definition.AutoBrand))(Json)
  19:              Return AutoBrand
  20:          End Using
  21:      End Function
  22:   
  23:   
  24:  End Class

І далі, власно кажучи, сам клієнт, який завантажує всі інтерфейси та викликає операції.


   1:  Module Start
   2:   
   3:      'мета патерну у тому Dependency Injection у данному випадку в тому, что ExportOperation.SaveData ничего не знає про реалізацію DJ.Service.InsertService
   4:      'в ExportOperation.SaveData є kernel.Get(Of DJ.Service.InsertService)
   5:      'завантаження робиться методом DJ.Service.InsService.InsertBrand - але реалізація InsertBrand при зверненні невідома
   6:      'необхідно знати тільки загальний інтерфейс DJ.Service.InsertService
   7:   
   8:      Sub Main()
   9:          Dim NinjectKernel As Ninject.IKernel = New Ninject.StandardKernel(New ExportModule())
  10:          Dim ExportOperation As New ExportOperation(NinjectKernel)
  11:          ExportOperation.SaveData()
  12:          System.Console.WriteLine("DB initialize succesfully")
  13:          Console.ReadKey()
  14:      End Sub
  15:   
  16:  End Module

Додатково почитати про Ninject ви можете тут:


4. Проєкт #5 - тести.

Отже, поздоровляю вас, друзі! Ми вже змогли зробити з двух-трьх стрічок чотири проекта з десятками стрічок коду! І цей код буде виконуватися у тисячі разів повільніше! І тепер ніхто з програмістів, які роблять окремі частки коду - вже не розуміє - що робіть цей код взагалі! А робить він лише загрузку даних у таблу, операцію на дві стрічки коду, з якої ми почали.

Але далі я покажу як зробити розробку програмного забезпечення ще втричі повільніше, за допомогою застосування патерна TDD (test driven development). Як завантажити програмістів додатковими задачами ще і ще більше!



Нажаль, друзі, я вичерпав свій вільний час, продовжимо іншого разу...




Ось тут продовження - Unit-тести для ASP.NET MVC.





Comments ( )
Link to this page: //www.vb-net.com/Ninject/index.htm
< THANKS ME>