ინვერსიის პრინციპი. რა არის დამოკიდებულების ინვერსიის პრინციპი და რატომ არის ის მნიშვნელოვანი? ფენიანი არქიტექტურა დამოკიდებულების ინვერსიით

პასუხისმგებლობის უარყოფა: ამ სტატიის ავტორს არ აქვს მიზანი, შეარყიოს ავტორიტეტი ან რაიმე სახით შეურაცხყოფა მიაყენოს ისეთ პატივცემულ ამხანაგს, როგორიც არის „ბიძა“ ბობ მარტინი. ეს უფრო მეტად ეხება დამოკიდებულების ინვერსიის პრინციპზე უფრო ფრთხილად ფიქრს და მის აღწერისთვის გამოყენებული მაგალითების ანალიზს.

მთელი სტატიის განმავლობაში მოგაწოდებთ ყველა საჭირო ციტატას და მაგალითს ზემოაღნიშნული წყაროებიდან. მაგრამ იმისათვის, რომ თავიდან ავიცილოთ „სპოილერები“ და თქვენი აზრი ობიექტური დარჩეს, გირჩევთ 10-15 წუთი დახარჯოთ და წაიკითხოთ ამ პრინციპის ორიგინალური აღწერა სტატიაში ან წიგნში.

დამოკიდებულების ინვერსიის პრინციპი ასე გამოიყურება:

ა. უმაღლესი დონის მოდულები არ უნდა იყოს დამოკიდებული ქვედა დონის მოდულებზე. ორივე უნდა იყოს დამოკიდებული აბსტრაქციებზე.
B. აბსტრაქციები არ უნდა იყოს დამოკიდებული დეტალებზე. დეტალები უნდა იყოს დამოკიდებული აბსტრაქციებზე.
დავიწყოთ პირველი პუნქტით.

ფენების დალაგება

ხახვს აქვს ფენები, ნამცხვრებს აქვს ფენები, კანიბალებს აქვთ ფენები და პროგრამულ სისტემებს აქვთ ფენები! - შრეკი (გ)
ნებისმიერი კომპლექსური სისტემა იერარქიულია: თითოეული ფენა აგებულია აპრობირებული და კარგად მოქმედი ქვედა დონის ფენის საფუძველზე. ეს საშუალებას გაძლევთ ფოკუსირება მოახდინოთ ცნებების შეზღუდულ კომპლექტზე ერთდროულად, ისე, რომ არ იფიქროთ იმაზე, თუ როგორ არის განხორციელებული ძირითადი ფენები.
შედეგად, ჩვენ ვიღებთ შემდეგ დიაგრამას:

სურათი 1 - "გულუბრყვილო" ფენების სქემა

ბობ მარტინის თვალსაზრისით, სისტემის ფენებად დაყოფის ასეთი სქემაა გულუბრყვილო. ამ დიზაინის მინუსი არის „მზაკვრული თვისება: ფენა პოლიტიკადამოკიდებულია ცვლილებებზე ყველა ფენაში გზაზე კომუნალური. ეს დამოკიდებულება გარდამავალია.» .

ჰმ... ძალიან უჩვეულო განცხადება. თუ ვსაუბრობთ .NET პლატფორმაზე, მაშინ დამოკიდებულება გარდამავალი იქნება მხოლოდ იმ შემთხვევაში, თუ მიმდინარე მოდული „გამოავლენს“ უფრო დაბალი დონის მოდულებს მის საჯარო ინტერფეისში. სხვა სიტყვებით რომ ვთქვათ, თუ შიგნით მექანიზმიᲤენაარის საჯარო კლასი, რომელიც არგუმენტად იღებს მაგალითს StringUtil(დან კომუნალურიᲤენა), შემდეგ დონის ყველა კლიენტი მექანიზმიᲤენაგახდეს დამოკიდებული კომუნალურიᲤენა. წინააღმდეგ შემთხვევაში, ცვლილებების გარდამავალი არ არის: ქვედა დონის ყველა ცვლილება შემოიფარგლება მიმდინარე დონით და არ ვრცელდება ზემოთ.

ბობ მარტინის იდეის გასაგებად, უნდა გახსოვდეთ, რომ დამოკიდებულების ინვერსიის პრინციპი პირველად 1996 წელს იყო აღწერილი და C++ ენა გამოიყენეს მაგალითებად. თავდაპირველ სტატიაში, თავად ავტორი ამას წერს ტრანზიტულობის პრობლემა არსებობს მხოლოდ ენებზე, კლასობრივი ინტერფეისის მკაფიო განცალკევების გარეშე. C ++ -ში, გარდამავალი დამოკიდებულების პრობლემა ნამდვილად აქტუალურია: თუ ფაილი PolicyLayer. მოიცავს დირექტივის "შერთვის" მეშვეობით მექანიზმის ფენა. , რომელიც თავის მხრივ მოიცავს UtilityLayer. , შემდეგ სათაურის ფაილში ნებისმიერი ცვლილებისთვის UtilityLayer. (თუნდაც ამ ფაილში გამოცხადებული კლასების "დახურულ" განყოფილებაში) ჩვენ მოგვიწევს ყველა კლიენტის ანაზღაურება და განმეორება. ამასთან, C ++ - ში ეს პრობლემა მოგვარებულია PIML იდიომის გამოყენებით, რომელიც შემოთავაზებულია Herb Sutter– ის მიერ და ახლა არც ისე აქტუალურია.

ამ პრობლემის მოგვარება ბობ მარტინის თვალსაზრისით არის ეს:

”უფრო მაღალი დონის ფენა აცხადებს აბსტრაქტულ ინტერფეისს მისთვის საჭირო სერვისებზე. შემდეგ ქვედა ფენები ხორციელდება ამ ინტერფეისების დასაკმაყოფილებლად. ზედა დონეზე მდებარე ნებისმიერი კლასი მის გვერდით ფენის ფენას ახდენს აბსტრაქტული ინტერფეისით. ამრიგად, ზედა ფენები ქვედა ნაწილებისგან დამოუკიდებელია. პირიქით, ქვედა ფენები დამოკიდებულია აბსტრაქტული სერვისების ინტერფეისში, გამოაცხადაუფრო მაღალ დონეზე... ამრიგად, დამოკიდებულების შეცვლით, ჩვენ შევქმენით სტრუქტურა, რომელიც ამავე დროს უფრო მოქნილი, გამძლე და მობილურია



სურათი 2 - ინვერსიული ფენები

გარკვეულწილად, ამ დაყოფას აზრი აქვს. მაგალითად, დამკვირვებლის შაბლონის გამოყენებისას, ეს არის დაკვირვებული ობიექტი (დაკვირვებადი), რომელიც განსაზღვრავს ინტერფეისს გარე სამყაროსთან ურთიერთქმედებისთვის, ასე რომ, მასზე რაიმე გარეგანი ცვლილება გავლენას არ მოახდენს.

მაგრამ მეორეს მხრივ, როდესაც საქმე ეხება კონკრეტულად ფენებს, რომლებიც ჩვეულებრივ წარმოდგენილია როგორც ასამბლეები (ან პაკეტები UML თვალსაზრისით), შემოთავაზებულ მიდგომას ძნელად შეიძლება ეწოდოს სიცოცხლისუნარიანობა. განმარტებით, ქვედა დონის დამხმარე კლასები გამოიყენება უმაღლესი დონის ათეულ სხვადასხვა მოდულში. კომუნალური ფენაგამოყენებული იქნება არა მხოლოდ მექანიზმის ფენა, არამედ შიგნით მონაცემთა წვდომის ფენა, სატრანსპორტო ფენა, სხვა ფენა. უნდა განახორციელოს თუ არა ყველა უმაღლესი დონის ყველა მოდულში განსაზღვრული ინტერფეისები?

ცხადია, ეს გამოსავალი ძნელად იდეალურია, მით უმეტეს, რომ ჩვენ ვწყვეტთ პრობლემას, რომელიც არ არსებობს ბევრ პლატფორმაზე, როგორიცაა .NET ან Java.

აბსტრაქციის ცნება

ბევრი ტერმინი ისე იბადება ჩვენს ტვინში, რომ მათ ყურადღებას აღარ ვაქცევთ. "ობიექტზე ორიენტირებული" პროგრამისტების უმეტესობისთვის ეს ნიშნავს, რომ ჩვენ ვწყვეტთ ფიქრს ბევრ ზედმეტად გამოყენებულ ტერმინებზე, როგორიცაა "აბსტრაქცია", "პოლიმორფიზმი", "ენკაფსულაცია". რატომ უნდა იფიქროთ მათზე, რადგან ყველაფერი უკვე ნათელია? ;)

თუმცა, იმისთვის, რომ ზუსტად გავიგოთ დამოკიდებულების ინვერსიის პრინციპისა და განმარტების მეორე ნაწილის მნიშვნელობა, ჩვენ უნდა დავუბრუნდეთ ერთ-ერთ ამ ფუნდამენტურ კონცეფციას. მოდით შევხედოთ ტერმინი „აბსტრაქციის“ განმარტებას გრადი ბუჩას წიგნიდან:

აბსტრაქცია განსაზღვრავს ზოგიერთი ობიექტის არსებით მახასიათებლებს, რომლებიც განასხვავებს მას ყველა სხვა ტიპის ობიექტისგან და, ამრიგად, მკაფიოდ განსაზღვრავს მის კონცეპტუალურ საზღვრებს დამკვირვებლის თვალსაზრისით.

სხვა სიტყვებით რომ ვთქვათ, აბსტრაქცია განსაზღვრავს ობიექტის ხილულ ქცევას, რომელიც პროგრამირების ენების ტერმინებში განისაზღვრება ობიექტის საჯარო (და დაცული) ინტერფეისით. ძალიან ხშირად ჩვენ ვაწარმოებთ აბსტრაქციების მოდელირებას ინტერფეისების ან აბსტრაქტული კლასების გამოყენებით, თუმცა OOP-ის თვალსაზრისით ეს არ არის აუცილებელი.

დავუბრუნდეთ განმარტებას: აბსტრაქციები არ უნდა იყოს დამოკიდებული დეტალებზე. დეტალები უნდა იყოს დამოკიდებული აბსტრაქციებზე.

რა მაგალითი გვახსენდება ახლა, მას შემდეგ რაც გავიხსენებთ რა არის? აბსტრაქცია? როდის იწყება აბსტრაქცია დეტალებზე დამოკიდებული? ამ პრინციპის დარღვევის მაგალითია აბსტრაქტული კლასი GZipStream, რომელიც იღებს MemoryStream, არა აბსტრაქტული კლასი ნაკადი:

აბსტრაქტული კლასი GZipStream (// აბსტრაქცია GZipStream იღებს კონკრეტული ნაკადით დაცულ GZipStream(MemoryStream memoryStream) () )

ამ პრინციპის დარღვევის კიდევ ერთი მაგალითი იქნება აბსტრაქტული საცავის კლასი მონაცემების წვდომის ფენიდან, რომელიც იღებს კონსტრუქტორს. PostgreSqlConnectionან კავშირის სტრიქონი SQL Server-ისთვის, რომელიც აქცევს ამგვარი აბსტრაქციის ნებისმიერ განხორციელებას კონკრეტულ განხორციელებასთან დაკავშირებულს. მაგრამ ეს ნიშნავს ბობ მარტინს? თუ ვიმსჯელებთ სტატიაში ან წიგნში მოყვანილი მაგალითებით, ბობ მარტინი სულ სხვა რამეს ესმის „აბსტრაქციის“ კონცეფციით.

პრინციპიDIPმარტინის თქმით

მისი განმარტების ასახსნელად, ბობ მარტინი მოცემულია შემდეგ განმარტებას.

DIP პრინციპის ოდნავ გამარტივებული, მაგრამ მაინც ძალიან ეფექტური ინტერპრეტაცია გამოიხატება მარტივი ევრისტიკული წესით: „თქვენ უნდა იყოთ დამოკიდებული აბსტრაქციებზე“. იგი აცხადებს, რომ არ უნდა არსებობდეს დამოკიდებულება კონკრეტულ კლასებზე; პროგრამაში ყველა კავშირმა უნდა გამოიწვიოს აბსტრაქტული კლასი ან ინტერფეისი.

  • არ უნდა არსებობდეს ცვლადები, რომლებიც ინახავს კონკრეტულ კლასებზე მითითებებს.
  • არ უნდა იყოს კლასები, რომლებიც გამომდინარეობს კონკრეტული კლასებიდან.
  • არ უნდა არსებობდეს მეთოდები, რომლებიც არღვევს მეთოდს, რომელიც განხორციელებულია ერთ-ერთ საბაზო კლასში.

ზოგადად DIP პრინციპის დარღვევის საილუსტრაციოდ და, კერძოდ, პირველი „განმამარტებელი“ პუნქტი, მოცემულია შემდეგი მაგალითი:

საჯარო კლასის ღილაკი ( პირადი ნათურა; საჯარო void გამოკითხვა() (თუ (/* ზოგიერთი პირობის */) ნათურა. TurnOn(); ) )

ახლა კიდევ ერთხელ გავიხსენოთ რა არის ეს აბსტრაქციადა უპასუხეთ კითხვას: არის თუ არა აქ „აბსტრაქცია“, რომელიც დეტალებზეა დამოკიდებული? სანამ ამაზე ფიქრობთ ან ეძებთ აბზაცს, რომელიც შეიცავს ამ კითხვაზე პასუხს, მინდა მცირე გადახვევა გავაკეთო.

კოდს აქვს ერთი საინტერესო თვისება. იშვიათი გამონაკლისების გარდა, თავად კოდი არ შეიძლება იყოს სწორი ან არასწორი; არის თუ არა ეს შეცდომა ან ფუნქცია, დამოკიდებულია იმაზე, თუ რას მოელიან მისგან. მაშინაც კი, თუ არ არსებობს ფორმალური სპეციფიკაცია (რაც ნორმაა), კოდი არასწორია მხოლოდ იმ შემთხვევაში, თუ ის აკეთებს სხვა რამეს, გარდა იმისა, რისი გაკეთებაც საჭიროა ან განზრახული აქვს. სწორედ ამ პრინციპს უდევს საფუძვლად კონტრაქტის პროგრამირება, რომელშიც სპეციფიკაცია (განზრახვები) გამოიხატება პირდაპირ კოდით წინაპირობების, პოსტპირობებისა და ინვარიანტების სახით.

კლასს უყურებს ღილაკივერ გეტყვით, დიზაინი ხარვეზებულია თუ არა. დანამდვილებით შემიძლია ვთქვა, რომ კლასის სახელი არ ემთხვევა მის განხორციელებას. კლასს უნდა დაერქვას სახელი ლამპარის ღილაკიან წაშალეთ კლასიდან ღილაკიველი ნათურა.

ბობ მარტინი ამტკიცებს, რომ ეს დიზაინი ხარვეზებულია, რადგან „მაღალი დონის განაცხადის სტრატეგია არ არის გამოყოფილი დაბალი დონის განხორციელებისგან. აბსტრაქციები არ არის გამოყოფილი დეტალებისგან. ასეთი განცალკევების არარსებობის შემთხვევაში, უმაღლესი დონის სტრატეგია ავტომატურად დამოკიდებულია ქვედა დონის მოდულებზე, ხოლო აბსტრაქცია ავტომატურად დამოკიდებულია დეტალებზე."

ჯერ ერთი, მე ვერ ვხედავ "უმაღლესი დონის სტრატეგიებს" და "ქვედა დონის მოდულებს" ამ მაგალითში: ჩემი აზრით, კლასები ღილაკიდა ნათურაარიან აბსტრაქციის ერთსა და იმავე დონეზე (ყოველ შემთხვევაში მე ვერ ვხედავ საპირისპიროს არგუმენტებს). ის ფაქტი, რომ კლასი ღილაკივინმეს გაკონტროლება არ აქცევს მას უფრო მაღალ დონეზე. მეორეც, აქ არ არის „დეტალზე დამოკიდებული აბსტრაქცია“, არის „აბსტრაქციის დეტალებზე დამოკიდებული განხორციელება“, რაც სულაც არ არის იგივე.

მარტინის გამოსავალი არის:



სურათი 3 - "დამოკიდებულებების ინვერსია"

ეს გამოსავალი ჯობია? მოდით შევხედოთ…

დამოკიდებულებების ინვერსიის მთავარი უპირატესობა "მარტინის მიხედვით" არის საკუთრების ინვერსია. ორიგინალურ დიზაინში, კლასის შეცვლისას ნათურაკლასი უნდა შეიცვალოს ღილაკი. ახლა კლასი ღილაკი"ფლობს" ინტერფეისს ღილაკის სერვერი, მაგრამ ის ვერ შეიცვლება „დაბალი დონეების“ ცვლილებების გამო, მაგ ნათურა. პირიქით: კლასის შეცვლა ღილაკის სერვერიშესაძლებელია მხოლოდ Button კლასში ცვლილებების გავლენის ქვეშ, რაც გამოიწვევს ცვლილებებს კლასის ყველა შთამომავალში ButonServer!

დამოკიდებულების ინვერსია არის პროგრამირების ერთ-ერთი ყველაზე მნიშვნელოვანი იდიომა. რუსულენოვან ინტერნეტში ამ იდიომის (პრინციპის) საოცრად ცოტა აღწერაა. ასე რომ, გადავწყვიტე ვცადო აღწერა. მე გავაკეთებ მაგალითებს Java-ში; ამ დროისთვის ეს უფრო ადვილია ჩემთვის, თუმცა დამოკიდებულების ინვერსიის პრინციპი გამოიყენება პროგრამირების ნებისმიერ ენაზე.

ეს აღწერა შემუშავდა ვლადიმერ მატვეევთან ერთად ჯავის შემსწავლელი სტუდენტების კლასებისთვის მოსამზადებლად.

სხვა სტატიები ამ სერიიდან:

ნება მომეცით დავიწყებ "დამოკიდებულების" განმარტებით. რა არის დამოკიდებულება? თუ თქვენი კოდი იყენებს ზოგიერთ კლასს შინაგანად ან აშკარად მოუწოდებს რომელიმე კლასის ან ფუნქციის სტატიკურ მეთოდს, ეს არის დამოკიდებულება. ნება მომეცით აგიხსნათ მაგალითებით:

ქვემოთ, კლასი A, მეთოდის შიგნით, სახელად someMethod(), აშკარად ქმნის B კლასის ობიექტს და უწოდებს მის მეთოდს someMethodOfB()

საჯარო კლასი A ( void someMethod() ( B b = new B(); b.someMethodOfB(); ) )

ანალოგიურად, მაგალითად, კლასი B აშკარად წვდება System კლასის სტატიკურ ველებსა და მეთოდებს:

საჯარო კლასი B ( void someMethodOfB() ( System.out.println ("Hello World"); ) )

ყველა შემთხვევაში, როდესაც რომელიმე კლასი (ტიპი A) დამოუკიდებლად ქმნის რომელიმე კლასს (ტიპი B) ან აშკარად წვდება სტატიკურ ველებს ან კლასის წევრებს, ეს ე.წ. სწორიდამოკიდებულება. იმათ. მნიშვნელოვანია: თუ კლასი თავის შიგნით მუშაობს სხვა კლასთან, ეს არის დამოკიდებულება. თუ ის ასევე ქმნის ამ კლასს თავის შიგნით, მაშინ ეს სწორიდამოკიდებულება.

რა არის ცუდი პირდაპირ დამოკიდებულებებში? პირდაპირი დამოკიდებულებები ცუდია, რადგან კლასი, რომელიც დამოუკიდებლად ქმნის სხვა კლასს თავის შიგნით, "მჭიდროდ" არის მიბმული ამ კლასთან. იმათ. თუ ცალსახად წერია, რომ B = new B(); , მაშინ კლასი A ყოველთვის იმუშავებს B კლასთან და არცერთ სხვა კლასთან. ან თუ წერია System.out.println("..."); მაშინ კლასი ყოველთვის გამოვა System.out და არსად სხვაგან.

მცირე კლასებისთვის, დამოკიდებულებები არ არის საშინელი. ეს კოდი შეიძლება საკმაოდ კარგად იმუშაოს. მაგრამ ზოგიერთ შემთხვევაში, იმისათვის, რომ თქვენმა A კლასმა შეძლოს უნივერსალურად მუშაობა სხვადასხვა კლასის გარემოში, შეიძლება მოითხოვოს კლასების სხვა დანერგვა - დამოკიდებულებები. იმათ. მაგალითად, დაგჭირდებათ არა კლასი B, არამედ სხვა კლასი იგივე ინტერფეისით, ან არა System.out, მაგრამ მაგალითად, გამომავალი ლოგერში (მაგალითად log4j).

პირდაპირი ურთიერთობა შეიძლება გრაფიკულად იყოს ნაჩვენები შემდეგნაირად:

იმათ. როდესაც თქვენ შექმნით A კლასს თქვენს კოდში: A a = new A(); ფაქტობრივად, იქმნება არა მხოლოდ ერთი კლასი A, არამედ დამოკიდებული კლასების მთელი იერარქია, რომლის მაგალითი მოცემულია ქვემოთ მოცემულ სურათზე. ეს იერარქია არის „ხისტი“: ცალკეული კლასების საწყისი კოდის შეცვლის გარეშე, თქვენ არ შეგიძლიათ შეცვალოთ რომელიმე კლასი იერარქიაში. ამიტომ, კლასი A ასეთ განხორციელებაში ცუდად ადაპტირდება ცვალებად გარემოსთან. სავარაუდოდ, ის არ შეიძლება გამოყენებულ იქნას სხვა კოდში, გარდა იმ კონკრეტული კოდისა, რომლისთვისაც თქვენ დაწერეთ.

A კლასის სპეციფიკური დამოკიდებულებისგან გამოსაყოფად გამოიყენეთ დამოკიდებულების ინექცია. რა არის დამოკიდებულების ინექცია? კოდში საჭირო კლასის აშკარად შექმნის ნაცვლად, დამოკიდებულებები გადაეცემა A კლასს კონსტრუქტორის მეშვეობით:

საჯარო კლასი A ( კერძო საბოლოო B b; საჯარო A(B b) ( this.b = b; ) საჯარო void someMethod() (b.someMethodOfB(); ) )

რომ. კლასი A ახლა იღებს თავის დამოკიდებულებას კონსტრუქტორის მეშვეობით. ახლა, A კლასის შესაქმნელად, ჯერ უნდა შექმნათ მისი დამოკიდებული კლასი. ამ შემთხვევაში ეს არის B:

B b = ახალი B(); A a = ახალი A(b); a.someMethod();

თუ ერთი და იგივე პროცედურა მეორდება ყველა კლასისთვის, ე.ი. გადასცეთ D კლასის ინსტანცია B კლასის კონსტრუქტორს, მისი დამოკიდებულებები E და F D კლასის კონსტრუქტორს და ა.შ., შემდეგ მიიღებთ კოდს, რომელშიც ყველა დამოკიდებულება იქმნება საპირისპირო თანმიმდევრობით:

G g = new G(); H h = ახალი H(); F f = ახალი (g,h); E e = ახალი E(); D d = ახალი D(e,f); B b = ახალი B(d); A a = ახალი A(b); a.someMethod();

ეს შეიძლება იყოს ნაჩვენები გრაფიკულად შემდეგნაირად:

თუ შეადარებთ 2 სურათს - ზემოთ მოცემულ სურათს პირდაპირი დამოკიდებულებით და მეორე სურათს დამოკიდებულების ინექციით - ხედავთ, რომ ისრების მიმართულება შეიცვალა საპირისპიროდ. ამ მიზეზით, იდიომს უწოდებენ დამოკიდებულებების „ინვერსიას“. სხვა სიტყვებით რომ ვთქვათ, დამოკიდებულების ინვერსია ნიშნავს, რომ კლასი დამოუკიდებლად არ ქმნის დამოკიდებულებებს, არამედ იღებს მათ შექმნილ ფორმაში კონსტრუქტორში (ან სხვაგვარად).

რატომ არის დამოკიდებულების ინვერსია კარგი? დამოკიდებულების ინვერსიით, შეგიძლიათ შეცვალოთ ყველა დამოკიდებულება კლასში მისი კოდის შეცვლის გარეშე. ეს ნიშნავს, რომ თქვენი კლასი A შეიძლება მოქნილად იყოს კონფიგურირებული სხვა პროგრამაში გამოსაყენებლად, გარდა იმ პროგრამისა, რომლისთვისაც იგი თავდაპირველად დაიწერა. რომ. დამოკიდებულების ინვერსიის პრინციპი (ზოგჯერ მას უწოდებენ დამოკიდებულების ინექციის პრინციპს) არის გასაღები მოქნილი, მოდულარული, მრავალჯერადი გამოყენების კოდის შესაქმნელად.

დამოკიდებულების ინექციის მინუსი ასევე ჩანს ერთი შეხედვით - კლასების ობიექტები, რომლებიც შექმნილია ამ ნიმუშის გამოყენებით, შრომატევადი კონსტრუქციაა. ამიტომ, დამოკიდებულების ინექცია ჩვეულებრივ გამოიყენება ზოგიერთ ბიბლიოთეკასთან ერთად, რომელიც შექმნილია ამ ამოცანის გასაადვილებლად. მაგალითად, ერთ-ერთი Google Guice ბიბლიოთეკა. Სმ. .

ბოლო განახლება: 03/11/2016

დამოკიდებულების ინვერსიის პრინციპიდამოკიდებულების ინვერსიის პრინციპი გამოიყენება თავისუფლად დაწყვილებული ერთეულების შესაქმნელად, რომელთა გამოცდა, შეცვლა და განახლება მარტივია. ეს პრინციპი შეიძლება ჩამოყალიბდეს შემდეგნაირად:

უმაღლესი დონის მოდულები არ უნდა იყოს დამოკიდებული ქვედა დონის მოდულებზე. ორივე უნდა იყოს დამოკიდებული აბსტრაქციებზე.

აბსტრაქციები არ უნდა იყოს დამოკიდებული დეტალებზე. დეტალები უნდა იყოს დამოკიდებული აბსტრაქციებზე.

პრინციპის გასაგებად, განიხილეთ შემდეგი მაგალითი:

Class Book ( public string Text ( get; set; ) public ConsolePrinter Printer ( get; set; ) public void Print() ( Printer.Print(Text); ) ) class ConsolePrinter ( public void Print (string text) ( Console.WriteLine (ტექსტი); ))

Book კლასი, რომელიც წარმოადგენს წიგნს, იყენებს ConsolePrinter კლასს დასაბეჭდად. ამ განმარტებით, Book კლასი დამოკიდებულია ConsolePrinter კლასზე. უფრო მეტიც, ჩვენ მკაცრად განვსაზღვრეთ, რომ წიგნის დაბეჭდვა შესაძლებელია მხოლოდ კონსოლზე ConsolePrinter კლასის გამოყენებით. სხვა ვარიანტები, მაგალითად, პრინტერზე გამოტანა, ფაილში გამოტანა ან გრაფიკული ინტერფეისის ელემენტების გამოყენება - ეს ყველაფერი ამ შემთხვევაში გამორიცხულია. წიგნის ბეჭდვის აბსტრაქცია არ არის გამოყოფილი ConsolePrinter კლასის დეტალებისგან. ეს ყველაფერი დამოკიდებულების ინვერსიის პრინციპის დარღვევაა.

ახლა შევეცადოთ ჩვენი კლასები მივიყვანოთ დამოკიდებულების ინვერსიის პრინციპთან, გამოვყოთ აბსტრაქციები დაბალი დონის განხორციელებისგან:

ინტერფეისი IPrinter ( void Print(string text); ) class Book ( public string Text (get; set; ) public IPrinter Printer (get; set; ) public Book (IPrinter printer) ( this.Printer = printer; ) public void Print( ) ( Printer.Print(Text); ) ) class ConsolePrinter: IPrinter ( public void Print(string text) ( Console.WriteLine("Print to console"); ) ) class HtmlPrinter: IPrinter ( public void Print(string text) ( Console.WriteLine ("ბეჭდვა html-ში"); ) )

წიგნის ბეჭდვის აბსტრაქცია ახლა გამოყოფილია კონკრეტული განხორციელებისაგან. შედეგად, როგორც Book კლასი, ასევე ConsolePrinter კლასი დამოკიდებულია IPrinter აბსტრაქციაზე. გარდა ამისა, ახლა ჩვენ ასევე შეგვიძლია შევქმნათ IPrinter აბსტრაქციის დამატებითი დაბალი დონის იმპლემენტაციები და დინამიურად გამოვიყენოთ ისინი პროგრამაში:

Book book = new Book(new ConsolePrinter()); წიგნი.ბეჭდვა(); book.Printer = new HtmlPrinter(); წიგნი.ბეჭდვა();

დამოკიდებულების ინვერსიის პრინციპის ფორმულირება შედგება ორი წესისგან, რომელთა დაცვა უკიდურესად დადებითად მოქმედებს კოდის სტრუქტურაზე:

  • უმაღლესი დონის მოდულები არ უნდა იყოს დამოკიდებული ქვედა დონის მოდულებზე. ორივე უნდა იყოს დამოკიდებული აბსტრაქციაზე.
  • აბსტრაქციები არ უნდა იყოს დამოკიდებული დეტალებზე. დეტალები უნდა იყოს დამოკიდებული აბსტრაქციებზე.

თავიდან ეს არც თუ ისე მიმზიდველად ჟღერს და მკითხველი ალბათ უკვე მომზადებულია ძალიან მოსაწყენი სტატიისთვის ტერმინების თაიგულით, მეტყველების რთული ფიგურებითა და მაგალითებით, რომელთაგან მაინც არაფერია ნათელი. მაგრამ ამაოდ, რადგან, პრინციპში, დამოკიდებულების ინვერსიის პრინციპს ექვემდებარება, ყველაფერი ისევ სწორ გამოყენებამდე მოდის და მთავარი იდეის კონტექსტში - კოდის ხელახალი გამოყენება.

კიდევ ერთი კონცეფცია, რომელიც აქტუალური იქნება, არის ტიპების სუსტი დაწყვილება, ანუ ერთმანეთზე მათი დამოკიდებულების შემცირება ან აღმოფხვრა, რაც, ფაქტობრივად, მიიღწევა აბსტრაქციისა და პოლიმორფიზმის გამოყენებით. ეს, ფაქტობრივად, არის დამოკიდებულების ინვერსიის პრინციპის არსი.

ახლა მოდით შევხედოთ მაგალითს, რომელიც ნათლად აჩვენებს, თუ როგორ გამოიყურება ფხვიერი შეერთება მოქმედებაში.

ვთქვათ, გადავწყვიტეთ დაბადების დღის ტორტის შეკვეთა. ამისთვის მივედით მშვენიერ თონეში ქუჩის კუთხეში. გავარკვიეთ, შეეძლოთ თუ არა ტორტის გამოცხობა ჩვენთვის ხმამაღალი სახელით „ლუქსი“ და დადებითი პასუხი რომ მივიღეთ, შევუკვეთეთ. Ეს მარტივია.

ახლა მოდით გადავწყვიტოთ რა აბსტრაქციები უნდა იყოს შეტანილი ამ კოდში. ამისათვის უბრალოდ დაუსვით საკუთარ თავს რამდენიმე შეკითხვა:

  • რატომ ტორტი? ასევე შეგიძლიათ შეუკვეთოთ ტორტი ან კექსი
  • რატომ კონკრეტულად დაბადების დღეზე? რა მოხდება, თუ ეს იყო ქორწილი ან გამოსაშვები?
  • რატომ "ფუფუნება", რა მოხდება, თუ "ჭიანჭველა" ან "პრაღა" უფრო მომწონს?
  • რატომ ზუსტად ეს თონე და არა საკონდიტრო მაღაზია ქალაქის ცენტრში ან სადმე სხვაგან?

და თითოეული ეს „რა თუ“ და „რა თუ“ არის წერტილები, რომლებშიც საჭიროა კოდის გაფართოება და, შესაბამისად, აბსტრაქციების საშუალებით ტიპების თავისუფალი შეერთება და განსაზღვრა.

ახლა დავიწყოთ თავად ტიპების აგება.

ჩვენ განვსაზღვრავთ ინტერფეისს ნებისმიერი საკონდიტრო შედევრისთვის, რომლის შეკვეთაც შეგვიძლია.

ინტერფეისი IPastry (სტრიქონის სახელი (მიღება; კომპლექტი;))

და აქ არის კონკრეტული განხორციელება, ჩვენი შემთხვევისთვის - დაბადების დღის ტორტი. როგორც ხედავთ, ტრადიციულად დაბადების დღის ტორტი სანთლებს მოიცავს)))

კლასი დაბადების ტორტი: IPastry ( საჯარო int NumberOfCandles ( მიიღეთ; კომპლექტი; ) საჯარო სტრიქონი სახელი (მიღება; დაყენება; ) საჯარო გადაფარვის სტრიქონი ToString() ( return String.Format ("(0) with (1) candle nices", Name, NumberOfCandles ) ;))

ახლა თუ გვჭირდება ტორტი ქორწილისთვის ან უბრალოდ ჩაისთვის, ან გვინდა კექსი ან კრემის ღვეზელები, ჩვენ გვაქვს ძირითადი ინტერფეისი ყველასთვის.

შემდეგი კითხვაა, რით განსხვავდება ერთმანეთისგან საკონდიტრო ნაწარმი (რა თქმა უნდა, სახელის გარდა). რა თქმა უნდა, რეცეპტით!

და რეცეპტის კონცეფცია მოიცავს ინგრედიენტების ჩამონათვალს და სამზარეულოს პროცესის აღწერას. აქედან გამომდინარე, ჩვენ გვექნება ცალკე ინტერფეისი ინგრედიენტის კონცეფციისთვის:

ინტერფეისი IIingredient ( სტრიქონი IngredientName (get;set;) ორმაგი რაოდენობა (მიიღე; კომპლექტი;) სტრიქონი ერთეულები (მიიღე; კომპლექტი;)

და აი ჩვენი ნამცხვრის ინგრედიენტები: ფქვილი, კარაქი, შაქარი და ნაღები:

კლასი ფქვილი:IIingredient ( საჯარო სტრიქონი IngredientName (მიღება; კომპლექტი;) საჯარო ორმაგი რაოდენობა (მიიღე; კომპლექტი;) საჯარო სტრიქონი ერთეულები (მიიღე; კომპლექტი;) საჯარო სტრიქონი ხარისხი (მიიღე; კომპლექტი;)) კლასი კარაქი: IIingredient (საჯარო სტრიქონი IngredientName (მიიღე; კომპლექტი;) საჯარო ორმაგი რაოდენობა (მიიღე; კომპლექტი;) საჯარო სტრიქონი ერთეულები (მიიღე; კომპლექტი;)) კლასი შაქარი: IIინგრედიენტი (საჯარო სტრიქონი IngredientName (მიღება; კომპლექტი;) საჯარო ორმაგი რაოდენობა (მიიღე; კომპლექტი;) საჯარო სტრიქონი ერთეულები ( მიიღეთ; კომპლექტი; ) საჯარო სტრიქონი Kind ( მიიღეთ; კომპლექტი; ) ) კლასი Creme: IIingredient ( საჯარო სტრიქონი IngredientName (მიიღე; კომპლექტი; ) საჯარო ორმაგი რაოდენობა (მიიღე; კომპლექტი; ) საჯარო სტრიქონი ერთეულები (მიიღე; კომპლექტი; ) საჯარო ორმაგი ცხიმი (მიიღეთ; დააყენეთ;)

ინგრედიენტების ჩამონათვალი შეიძლება განსხვავდებოდეს სხვადასხვა რეცეპტებში და სხვადასხვა საკონდიტრო ნაწარმში, მაგრამ ჩვენი რეცეპტისთვის ეს სია საკმარისია.

ახლა დროა გადავიდეთ რეცეპტის კონცეფციაზე. რასაც ვამზადებთ, ნებისმიერ შემთხვევაში ვიცით რა ჰქვია, რა არის, რა ინგრედიენტები შედის კერძში და როგორ მოვამზადოთ.

ინტერფეისი IRecipe ( ტიპი PastryType ( get; set; ) string სახელი ( get; set;) IList ინგრედიენტები (მიიღე; კომპლექტი;) სტრიქონის აღწერა (მიიღე; კომპლექტი;) )

კონკრეტულად, კლასი, რომელიც წარმოადგენს დაბადების დღის ტორტის რეცეპტს, ასე გამოიყურება:

კლასი BirthdayCakeRecipe: IRecipe ( საჯარო ტიპი PastryType ( get; set; ) საჯარო სტრიქონი სახელი ( get; set;) საჯარო IList ინგრედიენტები ( მიიღეთ; კომპლექტი; ) საჯარო სტრიქონი აღწერა (მიღება; კომპლექტი; ) საჯარო BirthdayCakeReipe() (ინგრედიენტები = ახალი სია (); } }

ახლა მოდით გადავიდეთ ჩვენს მშვენიერ თონეზე ქუჩის კუთხეში.

რა თქმა უნდა, ჩვენ შეგვიძლია მივმართოთ ბევრ სხვა თონეს, ამიტომ ჩვენ განვსაზღვრავთ ამისთვის ძირითად ინტერფეისსაც. რა არის ყველაზე მნიშვნელოვანი თონესთვის? პროდუქტების გამოცხობის უნარი.

ინტერფეისი IBBakery ( IPastry Bake (IRecipe რეცეპტი); )

და აქ არის კლასი, რომელიც წარმოადგენს ჩვენს თონეს:

კლასი NiceBakeryOnTheCornerOFMyStreet ( ლექსიკონი მენიუ = ახალი ლექსიკონი (); საჯარო void AddToMenu(IRecipe რეცეპტი) ( if (!menu.ContainsKey(recipe.Name)) ( menu.Add(recipe.Name, რეცეპტი); ) else ( Console.WriteLine ("ის უკვე მენიუშია"); ) ) public IRecipe FindInMenu (სტრიქონის სახელი) ( if (menu.ContainsKey(სახელი)) ( დაბრუნების მენიუ; ) Console.WriteLine ("ბოდიში...ამჟამად არ გვაქვს " + სახელი); დაბრუნება null; ) საჯარო IPastry Bake (IRecipe რეცეპტი) ( if (რეცეპტი != null) ( IPastry pastry = Activator.CreateInstance(recipe.PastryType) როგორც IPastry; if (pastry != null) ( pastry.Name = რეცეპტი.Name; დაბრუნება საკონდიტრო როგორც IPastry; ) ) დააბრუნე ნული ;))

რჩება მხოლოდ კოდის ტესტირება:

საკლასო პროგრამა ( static void Main() ( // საცხობი კლასის შემთხვევის შექმნა var bakery = new NiceBakeryOnTheCornerOFMyStreet(); // ინგრედიენტების მომზადება რეცეპტისთვის var flour = new Flour() ( IngredientName = "Flour", რაოდენობა = 1.5 , ერთეული = "კგ"); var კარაქი = ახალი კარაქი() (ინგრედიენტის სახელი = "კარაქი", რაოდენობა = 0.5, ერთეული = "კგ"); ვარ შაქარი = ახალი შაქარი() (ინგრედიენტის სახელი = "შაქარი", რაოდენობა = 0.7 , ერთეული = "კგ"); var creme = new Creme() (IngredientName = "Creme", რაოდენობა = 1.0, Units = "ლიტრი"); //და ეს არის თავად რეცეპტი var weddingCakeRecipe = new BirthdayCakeRecipe() ( PastryType = typeof(BirthdayCake), Name = "Birthday Cake Luxury", აღწერა = "აღწერილობა, თუ როგორ უნდა გააკეთოთ ლამაზი დაბადების დღის ტორტი"); weddingCakeRecipe.Ingredients.Add(flour); weddingCakeRecipe.Ingredients.Add(karak); weddingCakeRecipe.Ingredient .დამატება(შაქარი); ქორწილის ნამცხვრის რეცეპტი.ინგრედიენტები.დამატება(კრემი); //ჩვენი ტორტის რეცეპტის დამატება საცხობის მენიუში.AddToMenu(weddingCakeRecipe); //ახლა შევუკვეთოთ!! BirthdayCake cake = bakery.Bake(bakery.FindInMenu("Birthday Cake Luxury")) როგორც Birthday Cake; //სანთლების დამატება ;) cake.NumberOfCandles = 10; //და აქ ვართ !!! Console.WriteLine(ტორტი); ) )

ახლა კიდევ ერთხელ გადავხედოთ მთელ კოდს და შევაფასოთ. კოდი საკმაოდ მარტივია, ტიპები, მათი აბსტრაქციები, მონაცემები და ფუნქციონალობა ნათლად არის გამოსახული, კოდი გაფართოებისა და ხელახალი გამოყენების საშუალებას იძლევა. თითოეული სეგმენტი შეიძლება უმტკივნეულოდ შეიცვალოს სხვა სეგმენტით, რომელიც ემთხვევა საბაზისო ტიპს და ეს არ დაარღვევს დანარჩენ კოდს.

თქვენ შეგიძლიათ დაუსრულებლად დაამატოთ ინგრედიენტების ტიპები, რეცეპტები სხვადასხვა სახის საკონდიტრო ნაწარმისთვის, შექმნათ სხვა კლასები, რომლებიც აღწერს თონეებს, საკონდიტრო მაღაზიებს და სხვა მსგავს დაწესებულებებს.

არ არის ცუდი შედეგი. და ეს ყველაფერი იმის წყალობით, რომ ჩვენ ვცდილობდით კლასები მინიმალურად ყოფილიყო ერთმანეთთან დაკავშირებული.

ახლა მოდით ვიფიქროთ დამოკიდებულების ინვერსიის პრინციპის დარღვევის შედეგებზე:

  1. სიხისტე (ძალიან რთული იქნებოდა სისტემაში რაიმე ცვლილების შეტანა, რადგან ყოველი ცვლილება შეეხო მის ბევრ სხვადასხვა ნაწილს).
  2. სისუსტე (როდესაც რაიმე ცვლილება ხდება სისტემის ერთ ნაწილზე, მისი სხვა ნაწილები ხდება დაუცველი და ზოგჯერ ეს არც ისე აშკარაა ერთი შეხედვით).
  3. უძრაობა (შეგიძლიათ დაივიწყოთ კოდის ხელახალი გამოყენება სხვა სისტემებში, რადგან მოდულები ერთმანეთთან მჭიდროდ არის დაკავშირებული).

ახლა გამოიტანეთ საკუთარი დასკვნები იმის შესახებ, თუ რამდენად სასარგებლოა დამოკიდებულების ინვერსიის პრინციპი კოდში. ვფიქრობ, პასუხი აშკარაა.