Қонақ үлгісі: Объект құрылымын өзгертпестен алгоритмді бөлу
Visitor pattern
Программалық үлгі: Зорлаушы (Visitor) – алгоритмді объект құрылымынан бөліп, кодты өзгертусіз жаңа операциялар қосуға мүмкіндік береді. Бағдарламалауда пайдалы!
Ағылшыншамен салыстырыңыз: абзацты басыңыз — түпнұсқа терезеде ашылады. Абзац астындағы EN түймесі оны мәтін ішінде көрсетеді.
Мазмұны
Кіріспе
Бағдарламалық жасақтама үлгісі
Software design pattern
Келуші үлгісі – алгоритмді объектілер құрылымынан бөлетін бағдарламалық жасақтама үлгісі. Осы бөлінудің арқасында жаңа операцияларды объектілердің қолданыстағы құрылымын өзгертпей-ақ қосуға болады. Бұл объектіге бағытталған бағдарламалау және бағдарламалық жасақтауда ашық/жабық принципін сақтаудың бір жолы. Құрылымында, келуші кластар отбасына жаңа виртуалды функцияларды кластарды өзгертпей қосуға мүмкіндік береді. Оның орнына, виртуалды функцияның барлық қажетті мамандануларын іске асыратын келуші класс жасалады. Келуші объектінің дереккөз нұсқасын кіріс ретінде қабылдайды және қос жіберу арқылы мақсатына жетеді. Суммалық түрлері мен үлгілерді сәйкестендіру мүмкіндігі бар бағдарламалау тілдері келуші үлгісінің көптеген пайдасын жояды, себебі келуші класс объектінің түріне оңай түрлене алады және жаңа объекті түрі анықталған жағдайда компилятор қатесін тудырады.
A visitor pattern is a software design pattern that separates the algorithm from the object structure. Because of this separation, new operations can be added to existing object structures without modifying the structures. It is one way to follow the open/closed principle in object oriented programming and software engineering. In essence, the visitor allows adding new virtual functions to a family of classes, without modifying the classes. Instead, a visitor class is created that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch. Programming languages with sum types and pattern matching obviate many of the benefits of the visitor pattern, as the visitor class is able to both easily branch on the type of the object and generate a compiler error if a new object type is defined which the visitor does not yet handle.
Қолдану
2D компьютерлік жобалау (CAD) жүйесінің құрылымын қарастырайық. Оның негізгі түрінде шеңбер, сызық және доға сияқты негізгі геометриялық пішіндерді бейнелейтін бірнеше типтер бар. Объектілер қабаттарға бөлінген, ал типтер иерархиясының жоғарғы жағында сурет орналасқан, ол жай ғана қабаттар тізімі және бірнеше қосымша қасиеттерден тұрады. Бұл типтік иерархиядағы негізгі операция – суретті жүйенің түпкілікті файл пішіміне сақтау. Көріп тұрғанымыздай, иерархиядағы барлық типтерге жергілікті сақтау әдістерін қосу қанағаттанарлық көрінеді. Бірақ суреттерді басқа файл пішімдеріне сақтау да қажет. Көптеген файл пішімдеріне сақтау үшін әдістерді үстемелеп қосу, бастапқыдағы таза геометриялық дерек құрылымын тез арада бұзады. Мұны шешудің қарапайым жолы – әр файл пішімі үшін жеке функцияларды жасау. Мұндай сақтау функциясы суретті кіріс ретінде қабылдап, оны қарап шығып, нақты файл пішіміне түрлендіреді. Осылайша, әр формат қосылған сайын функцияларда қайталау жинақталады. Мысалы, растрлық форматта шеңбер пішінін сақтау үшін қолданылатын код, растрлық форматтың нақты түріне қарамастан, өте ұқсас болады және басқа бастапқы пішіндерден ерекшеленеді. Сызықтар мен көпбұрыштар сияқты басқа бастапқы пішіндерге де осыған ұқсас жағдай тән. Нәтижесінде, код объектілерді аралап өтетін үлкен циклға және цикл ішінде объектінің түрін анықтайтын үлкен шешім ағашына айналады. Бұл тәсілдің тағы бір кемшілігі – пішіннің бір немесе бірнеше сақтаушыларда қалып қоюы немесе жаңа бастапқы пішін енгізілгенде, сақтау процедурасы тек бір файл түрі үшін ғана жасалуы мүмкін, ал қалғандары үшін емес, бұл кодты кеңейту және күтіп ұстау қиындықтарына әкеледі. Файлдың нұсқалары көбейген сайын оны күтіп ұстау одан да қиындай түседі. Оның орнына, келуші үлгісін қолдануға болады. Ол логикалық операцияны (мысалы, save(image tree)) бүкіл иерархия бойынша бір сыныпқа (мысалы, Saver) енгізеді, ол ағашты аралап өтуге арналған жалпы әдістерді іске асырады және форматқа қатысты мінез-құлықтарды іске асыру үшін виртуалды көмекші әдістерді (мысалы, save circle, save square және т.б.) сипаттайды. CAD мысалында, мұндай форматқа қатысты мінез-құлықтарды Visitor (мысалы, SaverPNG) класының кіші класы іске асырады. Осылайша, типті тексерулер мен аралап өту қадамдарының барлық қайталануы жойылады. Сонымен қатар, енді компилятор пішіннің қалып қойған жағдайында шағымданады, өйткені ол жалпы базалық аралап өту/сақтау функциясынан күтіледі.
Consider the design of a 2D computer aided design (CAD) system. At its core, there are several types to represent basic geometric shapes like circles, lines, and arcs. The entities are ordered into layers, and at the top of the type hierarchy is the drawing, which is simply a list of layers, plus some added properties. A fundamental operation on this type hierarchy is saving a drawing to the system's native file format. At first glance, it may seem acceptable to add local save methods to all types in the hierarchy. But it is also useful to be able to save drawings to other file formats. Adding ever more methods for saving into many different file formats soon clutters the relatively pure original geometric data structure. A naive way to solve this would be to maintain separate functions for each file format. Such a save function would take a drawing as input, traverse it, and encode into that specific file format. As this is done for each added different format, duplication between the functions accumulates. For example, saving a circle shape in a raster format requires very similar code no matter what specific raster form is used, and is different from other primitive shapes. The case for other primitive shapes like lines and polygons is similar. Thus, the code becomes a large outer loop traversing through the objects, with a large decision tree inside the loop querying the type of the object. Another problem with this approach is that it is very easy to miss a shape in one or more savers, or a new primitive shape is introduced, but the save routine is implemented only for one file type and not others, leading to code extension and maintenance problems. As the versions of the same file grows it becomes more complicated to maintain it. Instead, the visitor pattern can be applied. It encodes the logical operation (i. e. save(image tree)) on the whole hierarchy into one class (i. e. Saver) that implements the common methods for traversing the tree and describes virtual helper methods (i. e. save circle, save square, etc.) to be implemented for format specific behaviors. In the case of the CAD example, such format specific behaviors would be implemented by a subclass of Visitor (i. e. SaverPNG). As such, all duplication of type checks and traversal steps is removed. Additionally, the compiler now complains if a shape is omitted since it is now expected by the common base traversal/save function.
Итерациялық циклдер
Келуші үлгісі Iterator үлгісі сияқты контейнерлік дерек құрылымдары бойынша итерация үшін қолданылуы мүмкін, бірақ мүмкіндіктері шектеулі. Мысалы, каталогтар құрылымы бойынша итерацияны дәстүрлі циклдың орнына функция класы арқылы жүзеге асыруға болады. Бұл, әр элемент үшін келуші функционалдығын іске асыру арқылы каталогтардың мазмұнынан түрлі пайдалы ақпаратты алуға және итерациялық кодты қайта пайдалануға мүмкіндік береді. Бұл үлгі Smalltalk жүйелерінде кеңінен қолданылады және C++ тілінде де кездеседі. Алайда, осы тәсілдің бір кемшілігі – циклдан оңай шығу мүмкін емес, сондай-ақ бір уақытта итерация жасау қиын (яғни, бір айнымалымен екі контейнерді параллель түрде қарау). Соңғысы үшін келушіге осы мүмкіндіктерді қолдау үшін қосымша функционалды жазу қажет болады.
The visitor pattern may be used for iteration over container like data structures just like Iterator pattern but with limited functionality. For example, iteration over a directory structure could be implemented by a function class instead of more conventional loop pattern. This would allow deriving various useful information from directories content by implementing a visitor functionality for every item while reusing the iteration code. It's widely employed in Smalltalk systems and can be found in C++ as well. A drawback of this approach, however, is that you can't break out of the loop easily or iterate concurrently (in parallel i. e. traversing two containers at the same time by a single variable). The latter would require writing additional functionality for a visitor to support these features.
Егжей-тегжейлер
Келуші үлгісі бірыңғай жіберуді қолдайтын бағдарламалау тілін қажет етеді, себебі көптеген объектіге бағытталған тілдер (мысалы, C++, Java, Smalltalk, Objective C, Swift, JavaScript, Python және C#) осы мүмкіндікке ие. Осы шарттар орындалғанда, екі объектіні қарастырайық, олардың әрқайсысы белгілі бір сыныпқа жатады; біреуі элемент, екіншісі – келуші деп аталады. Келуші, элементті аргумент ретінде қабылдайтын visit әдісін жариялайды, әрбір элемент класы үшін. Нақты келушілер келуші класынан туындайды және осы visit әдістерін іске асырады, олардың әрқайсысы объектілер құрылымымен жұмыс істейтін алгоритмнің бір бөлігін іске асырады. Алгоритмнің күйі нақты келуші класымен жергілікті түрде сақталады. Элемент келушіні қабылдау үшін accept әдісін жариялайды, келушіні аргумент ретінде қабылдайды. Элемент класынан туындаған нақты элементтер accept әдісін іске асырады. Ең қарапайым жағдайда, бұл келушінің visit әдісіне шақыру ғана болады. Бала объектілер тізімін сақтайтын композиттік элементтер әдетте олардың үстінен итерация жасайды, әр баланың accept әдісін шақырады. Клиент объектілер құрылымын тікелей немесе жанама түрде жасайды және нақты келушілерді құрады. Visitor үлгісін пайдалана отырып іске асырылатын операцияны орындау қажет болғанда, ол жоғарғы деңгейдегі элементтің accept әдісін шақырады. accept әдісі шақырылғанда, оның іске асырылуы элементтің динамикалық түрі мен келушінің статикалық түріне сүйене отырып таңдалады. Сәйкес visit әдісі шақырылғанда, оның іске асырылуы келушінің динамикалық түрі мен элементтің статикалық түріне сүйене отырып таңдалады, accept әдісінің іске асырылуынан белгілі болғандай, ол элементтің динамикалық түрімен бірдей. (Егер келуші берілген элемент түріне сәйкес аргументті өңдей алмаса, компилятор қатені анықтайды.) Осылайша, visit әдісін іске асыру элементтің және келушінің динамикалық түрлеріне сүйене отырып таңдалады. Бұл тиімді түрде қос жіберуді іске асырады. Объектілік жүйелері тек бір жіберуді ғана емес, бірнеше жіберуді де қолдайтын тілдер үшін, мысалы Common Lisp немесе C# (Dynamic Language Runtime (DLR) арқылы), келуші үлгісін іске асыру едәуір жеңілдетіледі (Dynamic Visitor деп те аталады), барлық қарастырылатын жағдайларды қамту үшін қарапайым функцияны жүктеуге рұқсат беру арқылы. Динамикалық келуші, егер ол тек ашық деректермен жұмыс істесе, ашық/жабық қағидасына (қолданыстағы құрылымдарды өзгертпейді) және бірыңғай жауапкершілік қағидасына (келуші үлгісін жеке компонентте іске асырады) сәйкес келеді. Осылайша, элементтердің графигін өтуге арналған бір алгоритм жазылуы мүмкін, ал элементтермен және келушілермен өзара әрекеттесу арқылы осы өту кезінде әртүрлі операцияларды орындауға болады, олардың динамикалық түрлеріне сәйкес әртүрлі келушілерді ұсыну арқылы.
The visitor pattern requires a programming language that supports single dispatch, as common object oriented languages (such as C++, Java, Smalltalk, Objective C, Swift, JavaScript, Python and C#) do. Under this condition, consider two objects, each of some class type; one is termed the element, and the other is visitor. The visitor declares a visit method, which takes the element as an argument, for each class of element. Concrete visitors are derived from the visitor class and implement these visit methods, each of which implements part of the algorithm operating on the object structure. The state of the algorithm is maintained locally by the concrete visitor class. The element declares an accept method to accept a visitor, taking the visitor as an argument. Concrete elements, derived from the element class, implement the accept method. In its simplest form, this is no more than a call to the visitor's visit method. Composite elements, which maintain a list of child objects, typically iterate over these, calling each child's accept method. The client creates the object structure, directly or indirectly, and instantiates the concrete visitors. When an operation is to be performed which is implemented using the Visitor pattern, it calls the accept method of the top level element(s). When the accept method is called in the program, its implementation is chosen based on both the dynamic type of the element and the static type of the visitor. When the associated visit method is called, its implementation is chosen based on both the dynamic type of the visitor and the static type of the element, as known from within the implementation of the accept method, which is the same as the dynamic type of the element. (As a bonus, if the visitor can't handle an argument of the given element's type, then the compiler will catch the error.) Thus, the implementation of the visit method is chosen based on both the dynamic type of the element and the dynamic type of the visitor. This effectively implements double dispatch. For languages whose object systems support multiple dispatch, not only single dispatch, such as Common Lisp or C# via the Dynamic Language Runtime (DLR), implementation of the visitor pattern is greatly simplified (a. k. a. Dynamic Visitor) by allowing use of simple function overloading to cover all the cases being visited. A dynamic visitor, provided it operates on public data only, conforms to the open/closed principle (since it does not modify extant structures) and to the single responsibility principle (since it implements the Visitor pattern in a separate component). In this way, one algorithm can be written to traverse a graph of elements, and many different kinds of operations can be performed during that traversal by supplying different kinds of visitors to interact with the elements based on the dynamic types of both the elements and the visitors.
Java үлгісі
Келесі мысал Java тілінде жазылған және түйіндер ағашының мазмұнын (осы жағдайда автомобильдің құрамдас бөліктерін сипаттайды) қалай басып шығаруға болатынын көрсетеді. Дөңгелек, Қозғалтқыш, Корпус және Автомобиль сияқты әрбір түйіннің түменші класы үшін жеке басып шығару әдістерін жасаудың орнына, бір ғана қонақ класы (CarElementPrintVisitor) қажетті басып шығару амалын орындайды. Түрлі түйіннің түменші кластары дұрыс басып шығару үшін сәл әртүрлі амалдарды қажет ететіндіктен, CarElementPrintVisitor өзінің visit әдісіне берілген аргументтің класына сәйкес амалдарды жібереді. CarElementDoVisitor, басқа файл пішіміне сақтау операциясы сияқты, солайша жұмыс істейді.
The following example is in the language Java, and shows how the contents of a tree of nodes (in this case describing the components of a car) can be printed. Instead of creating print methods for each node subclass (Wheel, Engine, Body, and Car), one visitor class (CarElementPrintVisitor) performs the required printing action. Because different node subclasses require slightly different actions to print properly, CarElementPrintVisitor dispatches actions based on the class of the argument passed to its visit method. CarElementDoVisitor, which is analogous to a save operation for a different file format, does likewise.
Python мысалы
Python классикалық мағынада әдіс жүктемесін қолдамайды (берілген параметрлердің типіне қарай полиморфтық мінез-құлық), сондықтан әртүрлі модель түрлері үшін "келу" әдістері әртүрлі атауларға ие болуы тиіс.
Python does not support method overloading in the classical sense (polymorphic behavior according to type of passed parameters), so the "visit" methods for the different model types need to have different names.
Шығысы
Сол жақ алдыңғы дөңгелекке бару. Оң жақ алдыңғы дөңгелекке бару. Сол жақ артқы дөңгелекке бару. Оң жақ артқы дөңгелекке бару. Көлік корпусына бару. Қозғалтқышқа бару. Көлікке бару. Сол жақ алдыңғы дөңгелегімді тепкілеу. Оң жақ алдыңғы дөңгелегімді тепкілеу. Сол жақ артқы дөңгелегімді тепкілеу. Оң жақ артқы дөңгелегімді тепкілеу. Денеімді жылжыту. Қозғалтқышты іске қосу. Көлігімді іске қосу.
Visiting front left wheel. Visiting front right wheel. Visiting back left wheel. Visiting back right wheel. Visiting body. Visiting engine. Visiting car. Kicking my front left wheel. Kicking my front right wheel. Kicking my back left wheel. Kicking my back right wheel. Moving my body. Starting my engine. Starting my car.