Programowanie obiektowe wg normy IEC 61131-3:2013
Marcin Zawisza drukuj
Koncepcja programowania obiektowego pojawiła się w latach 60. ubiegłego wieku i od tego czasu cieszy się niesłabnącą popularnością w środowisku informatyków. Większość współczesnych języków udostępnia je wprost lub jako opcjonalne rozszerzenie. Zupełnie inaczej wygląda sytuacja w przypadku aplikacji przemysłowych, których specyfika ogranicza tempo ewolucji sprzętu i metod programowania. Po części sprzyja temu także konserwatywne podejście niektórych producentów sprzętu, przez co brak jest odpowiednich narzędzi i ich standaryzacji. Trzecia wersja normy IEC 61131-3 stanowi próbę zmniejszenia dystansu między programistami IT a ich kolegami tworzącymi aplikacje dla PLC/PAC.
Ewolucja języków programowania to w dużej mierze zmiana sposobów modelowania problemów za pomocą komputera. W jej toku opis, związany ściśle ze sprzętem, zastąpiony został opisem rozwiązywanego z jego pomocą zagadnienia. Dzięki temu zamiast modelować maszynę (jak ma to miejsce na przykład w języku asemblera, który zmusza programistę do ciągłego myślenia o strukturze sprzętowej), nowoczesne języki programowania umożliwiają modelowanie mniej lub bardziej abstrakcyjnych problemów. Taka zmiana podejścia była możliwa między innymi dzięki opracowaniu języków obiektowych. Dostarczają one narzędzi, które umożliwiają przedstawienie problemu w bardziej intuicyjny sposób, zbliżony do tego, w jaki analizuje je nasz mózg. Opierają się one na założeniu, że otaczający nas świat składa się z obiektów, które charakteryzują się pewnymi cechami i zachowaniami i umożliwiają ich przedstawienie z pominięciem żmudnego procesu przystosowania do ograniczeń maszyny. Pozwalają one również w elegancki i zwięzły sposób opisać nawet bardzo skomplikowane zależności między tymi obiektami. Stworzona w obiektowy sposób aplikacja charakteryzuje się większą przejrzystością i co za tym idzie jest łatwiejsza w utrzymaniu i ewentualnej rozbudowie.
Podstawowe zagadnienia programowania obiektowego
Jak już wspomniano, podstawowym elementem otaczającej nas rzeczywistości jest obiekt, posiadający określone cechy i dopuszczalne zachowania. W programie jest on reprezentowany przez szczególny rodzaj struktury, która oprócz pól składowych (zwanych też właściwościami) przechowuje funkcje (nazywane w programowaniu obiektowym metodami). W typowej aplikacji przemysłowej przykładem obiektu może być szeroko rozumiany napęd. Może on mieć właściwości, takie jak np. moc i stan (określający w najprostszej wersji, czy jest załączony czy wyłączony) oraz metody czyli operacje, które można na nim wykonać, czyli np.: inicjuj(), załącz() i wyłącz() (dla odróżnienia metod od właściwości i podkreślenia, że mogą one zawierać parametry, ich nazwy będą zakończone pustym nawiasem).
Obiekt, tak jak zmienna musi mieć zdefiniowany typ, nazywany w tym przypadku jego klasą. Opisuje ona strukturę obiektu, to jest jego pola (właściwości) i dostępne funkcje (metody). Można więc powiedzieć, że obiekt jest egzemplarzem (lub inaczej instancją) swojej klasy czyli typu. Każdy obiekt potrzebuje odrębnej pamięci do przechowywania swojego aktualnego stanu (wartości pól) ale już metody są wspólne dla wszystkich obiektów danego typu. W rozpatrywanym przykładzie można sobie wyobrazić obiekty: napęd 1, napęd 2 i napęd 3, będące instancjami klasy napęd tak, jak to pokazano na rys. 1.
Klasy mogą być zagnieżdżane, co przydaje się do rozwiązywania bardziej złożonych problemów przez ich podział na mniejsze, łatwiejsze do opisania elementy. Można sobie wyobrazić na przykład klasę maszyna, która zawiera jeden lub kilka obiektów wspomnianej klasy napęd. Idąc dalej, klasa ciąg technologiczny może zawierać kilka obiektów klasy maszyna. W ten sposób za pomocą podejścia obiektowego, dokonujemy dekompozycji zagadnienia, odwzorowując jego rzeczywistą strukturę.
Drugim, poza zagnieżdżaniem, sposobem ponownego wykorzystania utworzonej już klasy do konstrukcji innej jest użycie mechanizmu zwanego dziedziczeniem (inheritance). Umożliwia on rozbudowanie funkcjonalności klasy przez utworzenie na jej podstawie nowej, w której dodaje się lub modyfikuje pewne elementy. Istniejąca klasa nazywana jest wtedy klasą bazową lub podstawową, a nowo tworzona pochodną. Klasa pochodna dziedziczy wszystkie cechy klasy podstawowej ale można do niej dodać nowe właściwości i metody oraz zmienić te odziedziczone, tworząc nowe o tej samej nazwie. Mechanizm ten nosi nazwę nadpisywania lub zasłaniania (overriding). Na rys. 2 pokazano przykładową hierarchię opartą na wcześniej utworzonej klasie napęd. Jak widać, klasa przełącznik G/T, będąca pochodną klasy napęd dodaje do jej funkcjonalności pole czas startu i modyfikuje wszystkie trzy metody. Sama natomiast jest bazą dla klasy softstart, która dodaje do niej pole limit prądu i ponownie modyfikuje wszystkie metody. Drugą klasą pochodną klasy napęd jest klasa falownik. Dodaje ona do pól klasy bazowej czas startu, czas zatrzymania, prędkość zadana i prędkość aktualna, modyfikuje wszystkie metody i dodaje dwie nowe ustaw prędkość() i pobierz prędkość().
Ponieważ klasy pochodne zawierają wszystkie pola i metody klasy bazowej (nawet, jeśli są one zmodyfikowane), dziedziczenie umożliwia traktowanie obiektów pochodnych tak, jakby były obiektami typu podstawowego. W naszym przykładzie wszystkie klasy mają pola moc i stan odziedziczone z klasy napęd, więc można się do nich odwoływać bez względu na typ obiektu. Można również zauważyć, że wspólne są dla nich metody inicjuj(), załącz() i wyłącz(), które jednak różnią się implementacją w poszczególnych typach pochodnych (ponieważ są w każdym z nich nadpisywane). Traktując obiekt typu pochodnego jak obiekt typu podstawowego, nie precyzuje się w momencie pisania programu, która konkretnie metoda (z której klasy) zostanie wykonana, co prowadzi do występowania tzw. polimorfizmu. Powiązanie wywołania metody z jej konkretną implementacją następuje dopiero na etapie uruchomienia programu, co nazywane jest wiązaniem dynamicznym lub późnym (dynamic/late binding). Jego przeciwieństwem jest wiązanie statyczne lub wczesne (static/early binding) czyli sytuacja, gdy już na etapie kompilacji programu można zidentyfikować konkretną wersję metody.
Jednym z podstawowych założeń programowania obiektowego jest hermetyzacja lub enkapsulacja (encapsulation). Oznacza ona w praktyce ukrywanie wszystkich elementów, które nie są potrzebne potencjalnemu użytkownikowi klasy. Zabezpiecza to ją przed niepożądaną modyfikacją (można np. wymusić dostęp do niektórych pól tylko za pomocą dedykowanych metod) oraz umożliwia modyfikację klasy bez modyfikacji kodu, który ją wykorzystuje. Jest to bardzo istotne np. w przypadku bibliotek zbudowanych w sposób obiektowy. Hermetyzacja wymaga zastosowania tzw. specyfikatorów dostępu, które określają „widoczność” poszczególnych elementów klasy (pól i metod). Podstawowe specyfikatory to: public dla elementów, które mają być widoczne poza klasą, private dla elementów, które mają być widoczne tylko w obrębie klasy (to jest dla jej metod) oraz protected dla elementów, które mają być widoczne tylko w obrębie klasy i jej klas pochodnych (dziedziczących). Przykładem hermetyzacji może być klasa falownik (rys. 2) Jeśli założymy, że dostęp do pola prędkość zadana jest ograniczony specyfikatorem private, to jego zmiana jest możliwa tylko za pomocą publicznej metody ustaw prędkość(). Takie rozwiązanie pozwala na kontrolę wpisywanej wartości, choćby celem sprawdzenia, czy nie przekracza ona dopuszczalnego zakresu lub nie jest w obszarze zakazanym, zagrażającym mechanice układu napędzanego. Podobnie odczyt aktualnej prędkości byłby możliwy tylko za pomocą metody pobierz prędkość().
Poza zwykłymi klasami, programowanie obiektowe zakłada także istnienie tzw. klas abstrakcyjnych, to jest takich, które nie mogą mieć swoich instancji (obiektów), mogą natomiast uczestniczyć w procesie dziedziczenia. Klasy abstrakcyjne muszą mieć co najmniej jedną metodę abstrakcyjną (tj. taką, która składa się wyłącznie z deklaracji i nie ma zdefiniowanego algorytmu), której implementację będą zawierały klasy pochodne. Patrząc na rys. 2 można dojść do wniosku, że klasą abstrakcyjną może być klasa napęd, ponieważ (jako zbyt ogólna) nie będzie wykorzystywana do tworzenia obiektów, ale stanowi podstawę do budowy innych klas. Metodami abstrakcyjnymi mogą być wtedy załącz() i wyłącz(), ponieważ ich implementacja w klasie napęd, jako zbyt ogólnej, nie ma sensu.
Norma IEC 61131 – stan aktualny
Norma IEC 61131 powstała aby uporządkować podstawowe zagadnienia dotyczące sterowników PLC, takie jak ich konstrukcja, konfiguracja, komunikacja z innymi urządzeniami czy sposób programowania. Jest to bardzo istotne, ponieważ rynek sterowników PLC rozrasta się szybko, a każdy z producentów przekonuje, że to jego rozwiązanie jest najlepsze i próbuje przyzwyczaić do niego jak najwięcej użytkowników. Promowanie przez rynek niekompatybilnych rozwiązań nie stoi w zgodzie z głównymi założeniami, decydującymi o wyższości sterowania opartego na PLC, to jest przede wszystkim elastyczności, łatwość diagnostyki ale i ewentualnej wymiany sprzętu. Bez jednolitego standardu narzuconego przez odpowiednią instytucję, taką jak Międzynarodowa Komisja Elektrotechniczna (International Electrotechnical Commission, IEC) trudno byłoby zapewnić możliwość łatwej migracji między różnymi platformami. Szczególne znaczenie ma tutaj trzecia część normy, której celem jest usystematyzowanie języków programowania, co ma ułatwić przenoszenie aplikacji między różnymi rozwiązaniami sprzętowymi, jeśli tylko zachowują one zgodność z tym standardem.
Dokument IEC 61131-3 i jego praktyczne zastosowanie, zostało opisane szczegółowo w cyklu artykułów pod tytułem „Programowanie sterowników PLC zgodnie z normą IEC 61131-3”, które ukazały się w numerach 3-7/2013 miesięcznika „Pomiary Automatyka Robotyka”. Od tego czasu pojawiła się jednak aktualizacja, mająca na celu między innymi wprowadzenie nowoczesnych narzędzi obecnych w większości współczesnych języków programowania. Zacznijmy od krótkiego przypomnienia struktury samego standardu. Aktualne jego elementy oraz informacja o ich ostatnich wersjach zostały zestawione w tab. 1.
Standard zawiera dziewięć niezależnie rozwijanych części, które jednak w swojej treści zawierają wzajemne odwołania. Pierwsza z nich to informacje ogólne, definicje i typowe właściwości funkcjonalne sterowników programowalnych odróżniające je od innych systemów (takie jak cykliczne przetwarzanie programu w oparciu o obraz wejść i wyjść, czy przydział czasu pracy na komunikację z programatorem). W części drugiej opisano elektryczne, mechaniczne i funkcjonalne wymagania dla sterowników oraz ich urządzeń peryferyjnych a także metody wykorzystywane do ich badania, warunki użytkowania, przechowywania i transportu. Określa ona także warunki środowiskowe oraz przedstawia klasyfikację sterowników i narzędzi programowania. Najbardziej interesująca nas część trzecia zawiera opis modelu programowego (w tym specyfikację dostępnych języków programowania z ich podziałem na tekstowe i graficzne) i komunikacyjnego oraz specyfikację elementów konfiguracyjnych. Część czwarta jest rodzajem przewodnika dla użytkowników PLC, którego zadaniem jest ich wsparcie za pomocą praktycznych wskazówek we wszystkich fazach projektowania systemu automatyki. Zasady komunikacji między sterownikami programowalnymi oraz innymi urządzeniami są opisane w części piątej, a szósta zawiera wymagania dla PLC i powiązanych urządzeń peryferyjnych, które mają być zastosowane jako elementy systemu bezpieczeństwa. W części siódmej został zdefiniowany język programowania aplikacji wykorzystujących sterowanie rozmyte. Część ósma jest rodzajem przewodnika, zawierającego wskazówki dotyczące zastosowania i implementacji języków programowania opisanych w części trzeciej. Wreszcie najnowsza w tym zestawieniu część dziewiąta specyfikuje sieć komunikacyjną dedykowaną dla małych czujników i urządzeń wykonawczych (SDCI), znaną powszechnie jako IO-Link.
Ostatnia wersja trzeciej część standardu została opublikowana w 2013 r., przy czym jest ona tak sformułowana, aby pozostać kompatybilną z poprzednią edycją. Poza poprawkami edycyjnymi dodano w niej nowe typy danych, funkcje konwersji i referencje (zmienne przechowujące wskaźniki do innych zmiennych). Najważniejsze modyfikacje dotyczą jednak struktury programu, która została zmieniona tak, aby umożliwić programowanie obiektowe zgodnie z współczesnymi standardami.
Struktura programu w aktualnej edycji IEC 61131-3
Norma IEC 61131-3 określa podstawowe składowe każdego programu PLC, nazwane w niej Jednostkami Organizacji Programu (Program Organization Unit, POU). Podział ten (dla wersji drugiej standardu) został przedstawiony w pierwszej części cyklu „Programowanie sterowników PLC zgodnie z normą IEC 61131-3.”, która ukazała się w numerze 3/2013 „Pomiary Automatyka Robotyka”. Wraz z nową edycją został on rozbudowany, aby umożliwić programowanie w sposób obiektowy, zachowując przy tym zgodność z wcześniejszą wersją (rys. 3). W obydwu edycjach występują elementy takie, jak: programy, funkcje i bloki funkcyjne, przy czym te ostatnie zostały obecnie wzbogacone o cechy „obiektowe”. Zupełną nowością jest natomiast pojawienie się czwartego rodzaju POU, to jest klas. Tak więc ciągle można tworzyć programy w sposób proceduralny, znany z drugiej edycji, wykorzystując tylko trzy pierwsze rodzaje POU i nie wykorzystując obiektowych cech bloków funkcyjnych. Można również cześć problemów rozwiązywać w sposób obiektowy, a inne klasycznie – mamy wtedy do czynienia z czymś, co w nomenklaturze IT nazywa się programowaniem wieloparadygmatowym.
Jednostki Organizacji Programu mają pewne wspólne cechy. Stanowią one formę pewnego rodzaju „kontenera”, zawierającego ściśle zdefiniowaną porcję kodu wraz z interfejsem łączącym go z innymi blokami oraz zmienne lokalne, niezbędne do jego wykonania. Cechą decydującą o zamknięciu danej części programu wewnątrz POU jest najczęściej realizowanie przez nią jakiejś możliwej do wyraźnego wyodrębnienia funkcji (np. oddzielenie sterowania pompami od regulacji temperatury). Często również stworzenie odpowiedniej funkcji lub bloku funkcyjnego umożliwia uniknięcie wielokrotnego powtarzania identycznych lub różniących się tylko parametrami sekcji programu. Istotna jest przy tym umiejętność dopasowania rodzaju POU do rozwiązywanego problemu, co wymaga z kolei znajomości ich możliwości i ograniczeń.
Podstawowym elementem organizującym kod aplikacji są programy. Ich wywołanie możliwe jest z poziomu zadań (tasks) sterownika, a same umożliwiają wywoływanie pozostałych POU. Programy nadają się idealnie do podziału projektu na mniejsze części, szczególnie, jeśli mają one być wykonywane z różnymi częstotliwościami albo mieć różne priorytety. Konstrukcja programu, tak jak i pozostałych jednostek organizacyjnych zawiera nagłówek i sekcję deklaracji po których znajduje się blok z kodem do wykonania (tab. 2).
Kolejnym elementem podziału logicznego kodu są funkcje. Ich cechą charakterystyczną jest fakt, że nie przechowują one swojego stanu między wywołaniami. Nie mogą więc mieć zmiennych statycznych w sekcji deklaracji i nie mają swoich instancji (obiektów). Inną właściwością wyróżniającą funkcje jest możliwość zwracania wartości jako wynik wywołania. Ponieważ funkcje nie przechowują żadnych informacji o swoim stanie wewnętrznym, to każde ich wywołanie z takimi samymi parametrami (wejściowymi, wejściowo-wyjściowymi oraz zmiennymi zewnętrznymi) da identyczny rezultat (to jest wartość funkcji oraz wartości parametrów wyjściowych, wejściowo-wyjściowych oraz zmiennych zewnętrznych). Podstawowym zastosowaniem tego rodzaju POU jest definiowanie prostych algorytmów, zwracających wynik natychmiast po wywołaniu. Dobrym przykładem mogą być funkcje standardowe służące do konwersji typów czy też wykonywania operacji arytmetycznych. Konstrukcję funkcji pokazano w tab. 3.
Bloki funkcyjne w odróżnieniu od funkcji przechowują swój stan, przez co umożliwiają implementację bardziej złożonych algorytmów z wykorzystaniem pamięci wewnętrznej (zmiennych statycznych). Ich głównym zadaniem jest wyodrębnienie ściśle zdefiniowanej części programu przeznaczonej do wielokrotnego wykorzystania. Deklaracja bloku funkcyjnego jest tak naprawdę opisem typu i aby móc go wykorzystać należy utworzyć obiekty tego typu, z których każdy powinien mieć unikalną nazwę. Przykładem standardowych bloków funkcyjnych mogą być liczniki i przekaźniki czasowe.
W aktualnej edycji standardu bloki funkcyjne zostały rozszerzone o elementy programowania obiektowego, które czynią je podobnymi do klas opisanych dalej. Do głównych nowości zalicza się możliwość definiowania metod składowych bloków funkcyjnych oraz mechanizm dziedziczenia, przy czym bloki funkcyjne mogą być pochodnymi zarówno innych bloków funkcyjnych jak i klas. Mogą one również implementować wcześniej stworzone interfejsy. Aktualny sposób deklaracji bloku funkcyjnego został pokazany w tab. 4.
Ponieważ zarówno deklaracje metod, jak i ciało bloku funkcyjnego mogą zostać pominięte, można wyróżnić trzy warianty bloków funkcyjnych. W przypadku braku definicji metod blok funkcyjny nie ma charakteru obiektowego i jest zgodny z poprzednią edycją normy. Jeśli natomiast zdefiniowano wewnątrz bloku tylko metody, a pominięto kod samego bloku, jest on równoważny klasie i może zostać nią zastąpiony. Ostatni przypadek to blok funkcyjny zawierający wszystkie dostępne elementy (metody i kod).
Jak już wspomniano, podstawowym elementem umożliwiającym realizację koncepcji programowania obiektowego jest klasa. Pozwala ona przechowywać dane (w postaci zmiennych składowych) oraz algorytmy (w postaci metod) w jednym obiekcie. Deklaracja klasy przypomina deklarację bloku funkcyjnego pozbawionego ciała (tab. 5) i jak wspomniano są to elementy równoważne.
Metody mogą być elementami składowymi bloków funkcyjnych oraz klas. Pod względem definicji przypominają funkcje i podobnie jak one mogą zwracać rezultat przez nazwę oraz nie przechowują stanu między wywołaniami. Jako składowe bloków funkcyjnych lub klas mają jednak możliwość odczytu i modyfikowania wartości ich zmiennych (wewnętrznych i statycznych).
Metody mogą wywoływać inne metody składowe wewnątrz bloku funkcyjnego lub klasy, używając w tym celu słowa kluczowego THIS. Jeśli należą do obiektów pochodnych, istnieje również możliwość wywołania w ich kodzie metod obiektu bazowego za pomocą słowa kluczowego SUPER. W przypadku pochodnych bloków funkcyjnych można uzyskać także dostęp do kodu bloku bazowego za pomocą słowa kluczowego SUPER().
Praktyczne wykorzystanie opisanych mechanizmów prezentuje prosty program realizujący załączanie i wyłączanie trzech silników, pierwszego za pomocą przełącznika gwiazda-trójkąt, drugiego za pomocą softstartu oraz trzeciego z użyciem falownika (listing 1). Do opisu tych trzech rodzajów konfiguracji wykorzystano hierarchię klas z rys. 2. Jej podstawę stanowi klasa abstrakcyjna Drive, która zawiera prywatną zmienną Power przechowującą informacje o mocy napędu oraz State, która przechowuje informacje o jego stanie (w postaci wartości typu wyliczeniowego DriveState). Konfiguracja mocy napędu możliwa jest za pomocą metody publicznej DriveInit(). Klasa zawiera poza tym dwie metody abstrakcyjne Start() i Stop() umożliwiające odpowiednio załączenie i wyłączenie napędu.
Klasa Drive jest podstawą dla klasy StarDeltaStarter, która rozszerza ją o dodatkowy parametr StartTime, zawierający czas startu (oznaczający na przykład opóźnienie przełączenia konfiguracji uzwojeń silnika). Większa liczba parametrów wymaga nowej metody konfiguracyjnej SDSInit(), która w swoim kodzie wykorzystuje metodę DriveInit() klasy bazowej. Ponieważ klasa StarDeltaStarter nie jest abstrakcyjna, musi zawierać implementacje metod Start() i Stop().
Na bazie StarDeltaStarter zdefiniowano z kolei klasę pochodną SoftStarter, która zawiera dodatkową zmienną CurrentLimit, określającą maksymalną krotność prądu podczas rozruchu, dodaje metodę konfiguracyjną SoftStarterInit() oraz nadpisuje metody Start() i Stop() instrukcjami odpowiednimi dla softstartu.
Drugą klasą pochodną klasy Drive jest Inverter. Dodaje ona do klasy bazowej zmienne AccelTime oraz DecelTime określające dynamikę zmian prędkości oraz CmdVelocity i ActVelocity zawierające odpowiednio prędkość zadaną i aktualną. Konfigurację umożliwia w tym przypadku metoda InverterInit(), a odczyt i ustawienie prędkości odpowiednio GetVelocity() i SetVelocity(). Tu również wymagana jest definicja metod Start() i Stop().
Po zdefiniowaniu powyższej rodziny klas, możliwe jest utworzenie w programie głównym trzech obiektów: SDS1 typu StarDeltaStarter, SoftStarter1 typu SoftStarter oraz Inverter1 typu Inverter. Obiekty te są w dalszej części programu inicjowane, a uruchomienie i zatrzymanie wymaga jedynie wywołania odpowiednich metod. Oczywiście przykładowy program zawiera dużo uproszczeń. Pominięto w nim szczegółowe implementacje metod Start() i Stop() zależne od wykorzystanego rozwiązania sprzętowego, ale stanowi dobrą ilustrację praktycznego wdrożenia podejścia obiektowego. Należy pamiętać, że norma nie ogranicza w żaden sposób używanego podczas programowania obiektowego języka. Metody klas czy też program, który je wywołuje mogą być równie dobrze zaimplementowane np. w drabince lub języku FBD.
Interfejsy i prototypowanie
Jak wspomniano wcześniej, zarówno klasy jak i bloki funkcyjne mogą być zdefiniowane jako abstrakcyjne (czyli posiadające co najmniej jedną metodę abstrakcyjną). Tak zdefiniowane typy nie mogą być wykorzystane do tworzenia obiektów a jedynie jako baza dla typów pochodnych. W szczególnym przypadku klasa lub blok funkcyjny mogą zawierać wyłącznie metody abstrakcyjne, tworząc rodzaj wzorca „zachowań” dla typów pochodnych. Realizacją takiej koncepcji jest interfejs (interface). Może on zawierać wyłącznie prototypy metod, które mogą zostać zdefiniowane dopiero w implementujących go typach (tab. 7).
Interfejsy, podobnie jak klasy i bloki funkcyjne podlegają mechanizmowi dziedziczenia. Umożliwia to budowanie całej hierarchii przez kolejne rozszerzanie interfejsów bazowych. Klasa lub blok funkcyjny może implementować jeden lub kilka interfejsów (co różni ten mechanizm od dziedziczenia). Wzajemne relacje między interfejsami, klasami i blokami funkcyjnymi przedstawia rys. 4.
Interfejs może również zostać użyty jak typ danych. Zmienne tego typu mogą zawierać referencje (wskazanie) do obiektów dowolnego typu implementującego ten interfejs lub pochodnego. Można także przypisać do nich wartość innej zmiennej tego samego typu (lub typu wskazującego na interfejs pochodny) oraz wartości NULL oznaczającą puste wskazanie.
Spróbujmy zatem zmodyfikować poprzedni przykład, wykorzystując opisane mechanizmy (listing 2). Ponieważ każdy obiekt ze stworzonej wcześniej hierarchii, bez względu na typ ma metody Start() i Stop(), można zdefiniować interfejs DriveControl zawierający prototypy tych metod. Jeżeli abstrakcyjna klasa bazowa Drive będzie implementowała ten interfejs, to również wszystkie pozostałe klasy, będące jej pochodnymi, będą go implementowały. Daje to bardzo ciekawe możliwości wykorzystane w dalszej części przykładu. Zdefiniowano tam trójelementową tablicę zawierającą pola typu DriveControl, aby następnie przypisać do nich referencje do stworzonych wcześniej zmiennych SDS1, SoftStarter1 i Inverter1. Tak skonfigurowaną tablicę wykorzystano do wykonania startu i zatrzymania urządzeń za pomocą pętli.
Oczywiście w tak małym przykładzie, dla zaledwie trzech elementów nie widać wyraźnej przewagi takiego rozwiązania, ale łatwo sobie wyobrazić obróbkę dużo większych tablic. Warto również zauważyć, że referencje przypisywane są w trakcie wykonywania programu i nic nie stoi na przeszkodzie, aby te przypisania modyfikować „w locie” (narzucając kompilatorowi wspomniane wcześniej dynamiczne wiązanie), co sprawia, że mamy do czynienia z niezwykle elastycznym rozwiązaniem. Jednowymiarową tablicę z napędami można równie dobrze zastąpić tablicą dwuwymiarową odwzorowującą na przykład sieć stanowisk do obróbki i zawierającą wskazania do znajdujących się akurat na nich elementach. Można nawet odwzorować trójwymiarowy układ (na przykład stanowiska na regałach w hali magazynowej czy też kontenery w porcie).
Zastosowanie interfejsów pozwala przewidywać zachowania typów, zanim jeszcze zostaną zdefiniowane a wykorzystanie referencji pozwala manipulować złożonymi obiektami równie łatwo jak zmiennymi typów podstawowych.
Przestrzenie nazw
Kolejnym elementem, który pojawił się w trzeciej edycji IEC61131-3, a o którym warto wspomnieć w kontekście programowania obiektowego są przestrzenie nazw (namespaces). Separują one pod względem nazewnictwa obszary kodu, umożliwiając użycie w nich tych samych identyfikatorów (nazw elementów). Odwołanie do elementu tak ograniczonego kodu, wymaga podania poza jego nazwą, nazwy przestrzeni w której został umieszczony (analogicznie jak w przypadku odwołania do elementu składowego klasy).
Przestrzenie nazw mogą być zagnieżdżane, przy czym wszystkie elementy programu, które nie są zamknięte w jednej z nich, znajdują się w zasięgu globalnym. Dotyczy to w szczególność standardowych funkcji i bloków funkcyjnych. Konstrukcja przestrzeni nazw została przedstawiona w tab. 8.
Deklaracja przestrzeni nazw może zawierać specyfikator INTERNAL, który wraz ze specyfikatorami elementów w niej zawartych modyfikuje ich dostępność według reguł zestawionych w tab. 9. Jak widać, elementy wewnętrznej przestrzeni nazw (tzn. zawierającej w definicji słowo INTERNAL), których dostępność nie została dodatkowo ograniczona (przez podanie specyfikatora innego niż PUBLIC) są widoczne tylko z przestrzeni o jeden poziom wyższej. Jeśli natomiast przestrzeń nazw jest publiczna (czyli nie ma w definicji ograniczenia dostępu), zawarte w niej elementy są widoczne w sposób ograniczony jedynie ich specyfikatorami.
Jak już wspomniano, odwołanie do elementów przestrzeni nazw wymaga podania nazwy tej przestrzeni oraz nazwy elementu (oddzielonych kropką, jak w przypadku dostępu do składowych klasy). Możliwe jest także wykorzystanie słowa kluczowego USING, po którym należy podać nazwę przestrzeni nazw. W bloku, w którym znajdzie się taka konstrukcja, możliwe jest odwoływanie się do elementów tej przestrzeni bez podawania jej nazwy.
Wprowadzenie przestrzeni nazw rozwiązuje odwieczny problem dużych aplikacji (szczególnie tych rozwijanych przez kilka osób), w których prędzej czy później dochodzi do konfliktu polegającego na wykorzystaniu przez więcej niż jedną osobę tego samego identyfikatora. Stworzenie hierarchicznego systemu nazewnictwa eliminuje to zagrożenie, poprawiając znacząco czytelność kodu. Trudno również przecenić ten mechanizm w przypadku tworzenia dużych bibliotek. Poza ich uporządkowaniem, przyspiesza on wyszukiwanie w nich elementów, oczywiście o ile struktura nazw zbudowana jest zgodnie z jasno określonymi zasadami.
Podsumowanie
Norma IEC 61131-3 w swojej trzeciej edycji niejako „odczarowuje” koncepcję programowania obiektowego, która do tej pory kojarzyła się raczej ze skomplikowanymi technikami wykorzystywanymi przez branżę IT. Oferuje ona bardziej „naturalne” podejście do sposobu organizacji programu i daje potężne narzędzia umożliwiające tworzenie przejrzystego, łatwego do utrzymania i dalszego rozwoju kodu, zachowując przy tym wszystkie „przemysłowe” cechy aplikacji. Co więcej nowe rozwiązania pozostają kompatybilne z dotychczasowymi, nie narzucając programiście żadnej konkretnej techniki, zamiast tego umożliwiając jej dopasowanie do jego umiejętności i stopnia komplikacji zagadnienia. Trudno więc przecenić te wprowadzone przez IEC mechanizmy i pozostaje mieć nadzieję na ich konsekwentne wdrażanie przez czołowych producentów sterowników PLC/PAC.
*Tabele dostępne są w wersji elektronicznej Miesięcznika Automatyka oraz w wersji drukowanej.
źródło: Automatyka 12/2018
Komentarze
blog comments powered by Disqus