(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 ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17
Link to this page: http://www.vb-net.com/Ninject/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <MAIL ME>  <ABOUT ME>  < THANKS ME>