Categories
C# Tekla

Refleksja o dzieleniu się

Wpis [part not set] z 2 w serii Tekla - wyrównanie przekrojów

W poprzednim poście wspomniałem o GitHub. W budownictwie co chwila ‘jaramy’ się nowym platformami CDE do zarządzania dokumentacją w toku inwestycji. Obiecują one łatwiejszą komunikację, wersjonowanie modeli/dokumentów, wykrywanie kolizji, ekstra premie za ukończenie projektu przed czasem. A nie, wróć, to budownictwo 😉

GitHub jest właśnie takim CDE, tylko, że darmowym. Oczywiście do pewnego zakresu funkcjonalności, jest też wersja płatna, ale spokojnie, nie dotrzemy do tego etapu. Z GitHub’a korzysta więcej niż 70 milionów programistów, pracując nad szeregiem projektów, w znacznej części publicznych. Robią to zazwyczaj już po godzinach swojej zwykłej pracy.

Dlaczego ktoś chciałby za darmo ‘pracować’ po godzinach? Co programista to inny powód, jednak najczęściej jest to chęć rozwijania się w innym kierunku niż ten aktualny w pracy. Programowanie to szereg wyborów, co człowiek to inne podejście i trudno żeby Twój pomysł zawsze był tym najlepszym. Dopiero konfrontacja z innymi ludźmi, z ich ideami daje możliwość spojrzenia na problem szerzej. W ten sposób rozwijasz swoje umiejętności, zarówno miękkie jak i twarde. Uczysz się współpracować i klepać lepsze, wydajniejsze programy. Poświęcając swój czas – zyskujesz.

Na GitHub znajdziesz wiele przykładów jak tworzyć rozszerzenia do Tekli. Rzuć okiem na repozytorium TSOpenAPIExamples lub na porfolio projektów Dawida Dyrcza..

Dobra, pogadałem, czas na konkrety: tu jest link do naszego repozytorium LetsConstructIT. Tam będą pojawiać się kolejne programy powstałe w ramach społeczności. Na tym etapie wiedz, że gdy okrzepniesz będziesz mógł wprowadzać zmiany do różnych repozytoriów. Obecnie poruszaj się raczej w obszarze read-only, czyli pobieraj kod źródłowy na swój komputer (Code -> Download ZIP).

Ok, pobrałeś repozytorium, rozpakowałeś ZIP’a, uruchomiłeś solucję TeklaApplications.sln w Visual Studio. Być może nawet skompilowałeś projekt SectionAligner. Warto żebyś wysłuchał opowieści o wersjach bibliotek.

Co roku zarówno producenci hardware jak i software wypuszczają nowe wersje swoich flagowców. Znak czasu, przejaw rozwiniętego kapitalizmu ktoś by powiedział. Nowe wersje = pieniądze za aktualizacje, wsparcie, itd. Waluta w gospodarce wędruje, wszyscy (no, prawie) są zadowoleni. Jak to przekłada się na programowanie? Niestety, nie operujemy w próżni, rozszerzenia Tekli opierają się na … Tekli, jej systemie wersji. Co roku, wraz z pojawieniem się nowej instancji publikowana jest kolejna wersja Tekla Open API. Na szczęście, jądro API Tekli jest stałe od wielu wersji. Bardzo rzadko zdarzają się wersje zmieniające funkcjonalność poprzednika. Skorzystamy z tego faktu w celu ułatwienia rozwoju nakładek.

W klasycznym schemacie kompilowanie pod różne Tekle wymaga przepinania wykorzystywanych bibliotek Tekla Open API na adekwatne wersje. Da się to zrealizować przez różne konfiguracje w Visual Studio, można pójść w stronę zaproponowaną przez Dawida Dyrcza, można też inaczej. W C# dostępny jest mechanizm przekierowań wersji wykorzystywanych bibliotek w trakcie uruchamiania programu. Czyli kompilujemy program celując np. w Teklą 2020 a uruchamiamy go pod Teklą 2018i. Lub jeśli jest taka potrzeba pod 2021. Wszystko dzięki elementowi <bindingRedirect> w konfiguracji programu.

Spójrz proszę w zawartość pliku App.config. Obecnie zawiera on wskazanie, że w trakcie uruchamiania programu należy używać wersji 2022.0.0.0 Teklowego API. Jeżeli np. pracujesz w Tekli 2016i to chcesz żeby Twój App.config wykorzystywał Teklowe biblioteki w wersji 2016.1.0.0. Zmień więc u siebie lokalnie zawartośc App.config stosownie do używanej wersji Tekli. Ciekawostką jest fakt, że ten mechanizm jest używany w ramach przygotowywania plików .tsep, czyli instalatorów Teklowych dodatków. Szerszy opis dostępny pod linkiem.

Dostosowałeś lokalnie App.config, możesz skompilować i uruchomić program. Tylko, właściwie jak to działa? Punktem wejścia do programu jest statyczna metoda Main znajdującą się w pliku Program.cs.

internal class Program
{
    static void Main(string[] args)
    {
        Aligner aligner = new Aligner();
        aligner.Execute();
    }
}

Jej jedyną odpowiedzialnością jest wywołanie metody Execute klasy Aligner, która dyryguje resztą procesu. Spójrzmy na tę klasę:

public class Aligner
{
    private DrawingHandler _dh;
    private Drawing _activeDrawing;

    public void Execute()
    {
        if (!InitailizeDrawingHandler())
            return;

        SectionAggregator sectionAggregator = new SectionAggregator(_dh, _activeDrawing);
        sectionAggregator.GetSectionsFromDrawing();
        sectionAggregator.SourceViews.ForEach(x => x.Align());

        _activeDrawing.CommitChanges();
    }

    private bool InitailizeDrawingHandler()
    {
        _dh = new DrawingHandler();
        if (!_dh.GetConnectionStatus())
            return false;

        _activeDrawing = _dh.GetActiveDrawing();
        if (_activeDrawing == null)
            return false;

        return true;
    }
}

Pierwsze co robi metoda Execute to (linijka 8 wywołującą 18-28) sprawdzenie czy znajdujemy się w obszarze rysunku. DrawingHandler to punkt wejścia do komunikacji z Teklowymi rysunkami. Przy jego pomocy możemy otwierać, zapisywać, edytować, drukować poszczególne rysunki. Jeżeli coś idzie nie tak z połączeniem z Teklą (nie jest włączona, wersje bibliotek są niezgodne, uruchomiona jest więcej niż jedna Tekla) to metoda DrawingHandler.GetConnectionStatus() zwróci false oznaczający brak połączenia. Dopasowywanie przekrojów ma sens tylko wtedy, gdy włączony jest rysunek – stąd sprawdzenie w linijka 24-26.

W wariancie pozytywnym: udane połączenie + rysunek aktywny, idziemy dalej, czyli w ramach metody Execute wywoływane są linijki 11-15. Tam napotykamy na wywołanie nowej klasy – SectionAggregator, która pozycjonuje przekroje zgodnie z zadaną logiką. Zachęcam do prześledzenia działania poszczególnych metod. Analizowanie działania algorytmów to podstawa do tworzenia własnych.

W ramach tego wpisu chcę poruszyć dwa różne scenariusze pobierania elementów z Tekli. W zalezności od potrzeb możesz potrzebować wszystkich obiektów danego typu, spełniających określony zestaw cech. Zrobiłem tak w metodzie GetAllSectionViews w ramach klasy SectionAggregator:

private IEnumerable<View> GetAllSectionViews()
{
    foreach (var item in _drawing.GetSheet().GetAllViews())
    {
        if (item is View)
        {
            View view = (View)item;
            if (view.ViewType == View.ViewTypes.SectionView)
                yield return view;
        }
    }
}

W powyższym listingu pobieram najpierw arkusz z danego rysunku. Arkuszem jest najwyższą instancją rysunkową do której wstawiane są kolejne widoki. I to własnie mając arkusz, mogę te widoki pobrać wykorzystując metodę GetAllViews. Następnie sprawdzam typ widoku – logika aplikacji wymaga w tym miejscu pobrania wyłącznie przekrojów, co zostało zapisane w linijce 8-ej.

Drugi schemat pobierania elementów to potrzeba wykorzystania selekcji, którą wykonał już użytkownik. Nie interesuje nas wszystko na rysunku, chcemy wiedzieć co jest obecnie zaznaczone. W tym celu powstała metoda GetOnlySelectedViews bazująca na pobraniu od DrawingHandler’a obiektu klasy DrawingObjectSelector. Dzieje się to w linijce 5-ej poniżej:

private List<SourceViewWithSections> GetOnlySelectedViews()
{
    List<SourceViewWithSections> sourceViews = new List<SourceViewWithSections>();

    foreach (var selectedObject in _dh.GetDrawingObjectSelector().GetSelected())
    {
        if (selectedObject is View)
        {
            View selectedView = selectedObject as View;
            if (selectedView.ViewType == View.ViewTypes.SectionView)
            {
                AddSectionViewWithRelatedSource(selectedView, sourceViews);
            }
            else
            {
                AddSectionViewsFromMainView(selectedView, sourceViews);
            }
        }
    }

    return sourceViews;
}

Wywołując GetSelected() na obiekcie DrawingObjectSelector pobieram aktualne zaznaczenie w obszarze rysunku. Dalej następuje sprawdzenie czy ten obiekt odpowiada naszym potrzebom – czy jest widokiem o typie ‘przekrój’.

Jak widzisz operujemy na bibliotece, którą dostarcza Trimble. Komunikujemy się z rysunkiem (w przyszłości z modelem) w ramach przewidzianych przez producenta programu. Ktoś w centrali dawno temu założył, że potrzebny jest DrawingHandler, który udostępni metodę GetSheet, która zwróci arkusz, z którego będziemy mogli to i owo. Kluczem jest zrozumienie, że ktoś poczynił pewne założenia jak to API ma działać i co ma oferować. I choć możliwości rozszerzania Tekli są znaczne, to mają swój kres – granicą są możliwości Tekla Open API, która jest furtką do bazy danych kryjącej pod schludnym interfejsem programu.

Żeby nie kończyć aż tak pesymistycznie (napotkasz na bariery, których nie skruszysz) chcę pokazać Ci dwie sztuczki, które możesz wykorzystać aby wydobyć więcej z API. Pierwszą z nich są metody rozszerzeniowe, czyli doklejki zwiększające możliwości jakie mają Teklowe obiekty. Spójrz proszę na screen:

Widzimy wywołanie metody GetId() na obiekcie klasy View. Ciekawostką jest fakt, że w oryginale klasa View nie posiada takiej metody. Dodałem ją jako metodą rozszerzeniową dodając nową moc do widoku:

public static class DrawingExtensions
{
    public static int GetId(this DatabaseObject view)
    {
        var identifier = (Tekla.Structures.Identifier)view.GetPropertyValue("Identifier");
        return identifier.ID;
    }
}

Deklarowanie metod rozszerzeniowych jest możliwe tylko w ramach statycznych klas. Sama metoda musi być statyczna a jej argument jest poprzedzony słowem kluczowym this. W ten sposób możesz dodawać nowe umiejętności klasom, które nie są Twoje, które pochodzą z zewnętrznych bibliotek.

W ramach tego rozszerzania wykorzystałem jeszcze jedną funkcjonalność C#. Tekla Open API nie eksponuje publicznie identyfikatorów (unikalnych adresów) obiektów rysunkowych. Ma je jednak wewnątrz swoich klas jako wewnętrzne właściwości. O proszę, tutaj są:

No i klops, przydałby nam sie unikalny identyfikator obiektu rysunkowego a ktoś kiedyś uznał, że nie powinien on być eksponowany jako publiczny. Na pewno miał ku temu powód (pewnie brak zaufania co do jego trwałości), ale my go potrzebujemy dosłownie na chwilkę – nie będziemy go zapisywać na przyszłość i wykorzystywać za tydzień. Jest on nam potrzebny w ramach tej sesji pracy w programie, więc hello – dajcie nam go!

Niestety, nikt nam go nie da – musimy pójśc po niego sami i właśnie do tego potrzebujemy tytułowej refleksji, którą wykorzystałem w ramach statycznej klasy ReflectionHelper. Dzięki niej jesteśmy w stanie w trakcie działania programu wydobyć to co ukryte – poznać wewnętrzny identyfikator obiektu rysunkowego.

Na ten moment wystarczy. Refleksja, metody roszerzeniowe, wspołpraca w ramach GitHub – pojawiło się kilka wartościowych nowości. W kolejnym wpisie przedstawię różne możliwości dystrybuowania Twoich Teklowych roszerzeń. Do usłyszenia!

Categories
Tekla C#

Ułatwmy sobie życie – rysunek w Tekli

Wpis [part not set] z 2 w serii Tekla - wyrównanie przekrojów

Doskonale pamiętam zgrzytanie zębami w trakcie mojej praktyki inżynierskiej, gdy jedyny feedback jaki otrzymywałem do wykonanego rysunku w Tekli brzmiał: przekroje nie są wyrównane do podstawowego widoku. Brzmi dla Ciebie abstrakcyjnie? Spójrz proszę na rysunek pobrany z oficjalnej dokumentacji programu:

Przekroje pochodzące z podstawowych widoków nr 1 i 2 są wyrównane w poziomie i pionie do źródła. Wygląda to ładnie, schludnie, po prostu inżyniersko. Tylko, że ich wyrównanie to klikologia, żmudne wskazanie rodzaju wyrównania (w pionie/poziomie) i wskazanie dwóch punktów określających wektor translacji widoku. Za pierwszym razem, gdy poznasz tę funkcję powiesz ‘wow’ – fajne, ułatwiające życie. Niestety przy setnym wyrównywaniu zaczniesz się zastanawiać, czy Twoje 5 lat studiowania było niezbędne do ‘ogarnięcia’ tej czynności.

Możesz teraz przewrotnie powiedzieć: po co to wyrównanie, nie ma takiej potrzeby, to strata czasu. Ale dla dobra tej serii proszę porzuć tę negację i zobacz co dla Ciebie przygotowałem:

W materiale video przedstawiłem działanie pierwszego, darmowego marka do Tekla Structures powstałego w ramach LetsConstructIT. Makro możesz pobrać z linku. Po wyświetleniu zawartości docelowej strony wybierz prawy przycisk myszy i skorzystaj z opcji Zapisz jako.

Tylko gdzie zapisać makro? Sprawdź w Tekli gdzie znajduje się folder z którego odczytywane są makra. W tym celu uruchom zaawansowane ustawienia:

A następnie w nowo otwartym oknie, w domyślnie otwartej zakładce File Locations odczytaj wartość zmiennej XS_MACRO_DIRECTORY:

W moim wypadku jest to ścieżka C:\TeklaStructures\2022.0\environments\common\macros. Skoro nasze makro jest dostępne z poziomu rysunku to powinniśmy je zapisać w podfolderze drawings powyższego katalogu. Czyli u mnie makro powinno zostać zapisane w: C:\TeklaStructures\2022.0\Environments\common\macros\drawings Pamiętaj, że w Twojej Tekli/środowisku ta ścieżka może być inna, ale odczytując aktualną wartość z zaawansowanych ustawień na pewno trafisz do brzegu.

Po zapisaniu źródłowego pliku Section Aligner.cs w folderze drawings i ponownym uruchomieniu Teklu będziesz miał do dyspozycji nowe makro rysunkowe o nazwie Section Aligner:

Narzędzie w zależności od wstępnego zaznaczenia posiada trzy tryby pracy:

  • wyrównanie wyłącznie zaznaczonego przekroju – przed uruchomieniem makra zaznacz jeden lub więcej przekrojów, w ten sposób określasz, które widoki mają zostać przesunięte,
  • wyrównanie wszystkich przekrojów pochodządzych od zaznaczonego widoku głównego – każdy przekrój ma swoje źródło, czyli widok na którym początkowo wskazany został zakres przekroju; zaznacz ten widok podstawowy a wyrównane zostaną wszyskie przekroje pochodne,
  • wyrównanie wszystkich przekrojów na rysunku – nie zaznaczaj niczego, odpal makro i ciesz się wykonaną pracą.

Ten skrypt jest dla mnie ważny z kilku powodów. Po pierwsze wydaje się całkiem funkcjonalny, przynajmniej dla niektórych użytkowników Tekli – wierzę, że zachęci to nowe osoby to choćby pobieżnego zapoznania się z programowaniem. Po drugie, w celu jego stworzenia musiałem wykorzystać kilka nowych dla Ciebie konceptów dostępnych w C# – będę o nich pisał w kolejnych postach, rozszerzając wachlarz Twoich umiejętności. Po trzecie, po przerwie, miałem okazję przypomnieć sobie wszystkie blaski i cienie Tekla Open API, które nadal uważam za podstawowy czynnik bardzo wysokiej ceny Tekli. Niestety, świadomość możliwości jakie niesie Tekla Open API jest niskie, nie mówiąc o umiejętnościach jego wykorzystania. Chęć nauczenia/wskazania tych nieodkrytych dróg była jednym z motorów powstania #LetsConstructIT.

Makro, którego dotyczy wpis jest zlepkiem kilku klas. Nie tworzyłem go jako jednego pliku – to był ostatni etap tworzenia. W kolejnym poście wskażę repozytorium na GitHub zawierające właściwe źródło. Zdecydowałem się na połączenie wszystkiego w jeden plik z uwagi na prostotę przekazania tego dalej – każdy użytkownik może dodać nowe makro do swojej Tekli, spiąć to z wypatrzonym skrótem klawiszowym i działać – do czego serdecznie Cię zachęcam 🙂

Categories
C#

Koniec podstaw – co dalej?

Wpis [part not set] z 8 w serii C# - podstawy

Poprzedni wpis był ostatnim, który planowałem w ramach cyklu o podstawach C#. Właściwie o początkach podstaw, bo posty temu poświęcone były raczej prześlizgnięciem się po wielu zagadnieniach.

Jeżeli dotychczas nie udało mi się obudzić w Tobie ciekawości ukierunkowanej na programowanie to … jeszcze się nie poddaję, jestem uparty z natury 😉 Kolejne wpisy będą przedstawiały konkretne problemy/zagadnienia z programów inżynierskich, które mogą zostać zoptymalizowane poprzez wykorzystanie C#. Wierzę, że dla praktyków to może być moment, kiedy poczują, że warto zdobyć nowe umiejętności.

Z kolei jeżeli jesteś w mniejszości i już na tym etapie czujesz dreszczyk ekscytacji to poniższy wpis kieruję właśnie do Ciebie. Postaram się przybliżyć w jaki sposób można czerpać wiedzę na temat programownia.

Na wstępie zaznaczam – nie istnieje magiczna, czerwona pigułka, której obejrzenie/przeczytanie zapewni Ci szybkie wejście w nową dziedzinie wiedzy. Przyczyny są dwie: ludzie uczą się na różne sposoby, jeden woli czytać posty na blogach takich jak te, inny preferuje filmy szkoleniowe na Youtube, trzeci chłonie wiedzę wyłącznie od innego człowieka, w trakcie zajęć na żywo. Powód drugi to fakt, że programowanie to nauka innego, maszynowego sposobu myślenie połączona z poznaniem nowego języka komunikacji. Trochę jakbyś miał nauczyć się chemii supramolekularnej po hiszpańsku. Nie wiem jak Ty, ale Ja nie znam hiszpańskiego, a chemia supramolekularna kojarzy mi z tradycyjną gruzińską biesiadą.

Gdy już wiesz, że droga jest długa i wyboista to warto zastanowić się nad motywacją. Nie będę tu coachował, spokojnie. Wrzucę tylko fancy grafikę, taką pod sociale. O, na przykład taką:

Płynąc do brzegu: jak można nauczyć się programowania w C#?

  • czytając oficjalną dokumentację Microsoftu – rozwiązanie idealne dla miłośników czytania instrukcji obsługi nowego odkurzacza.
  • oglądając materiały na Youtube – pytanie które, bo jest ich zatrzęsienie. Nawet ograniczając się wyłącznie do polskiego YT – do wyboru masz Ci treści na długie tygodnie oglądania. Pamiętaj, że od patrzenia ubędą Ci tylko tysięczne części dioptrii. Konieczne jest jednoczesne próbowanie, testowanie tego co pokazuje instruktor połączone z poszukiwaniem odpowiedzi na trudniejsze zagadnienia.
  • wykupujesz kurs na Udemy/Coursera – identycznie jak w przypadku YT, który z tysięcy kursów wybierzesz? Który z nich będzie odpowiadał na Twoje indywidualne potrzeby? Sam kilkukrotnie się sparzyłem, kiedy po ciekawej zapowiedzi kursu jego zawartość okazała się po prostu marna. Uwaga, poczekaj na promocje, kursy na powyższych platformach są notorycznie przecenianie do akceptowalnych 30-50 zł.
  • pokładasz zaufanie jednej platformy – Pluralsight, dzięki której uzyskujesz dostęp do setek kursów poświęconych różnym tematom. Płacisz więcej, masz kolejny abonament na głowie, ale dostajesz w zamian wysokie jakościowo, pogrupowane kursy. Zostaje kwestia pracy własnej, bez której nawet najlepszy kurs nie przyniesie efektów.
  • z ulgą dla portfela posiłkujesz się jednym z darmowych kursów na powyższych platformach (ich listę możesz znaleźć pod linkiem).
  • korzystasz z usług bootcampu. Od kilku lat jak grzyby na deszczu wyrastają kolejne firmy, które oferują, że zrobią z piekarza programistę. Płacisz grube pieniądze za dostęp do materiałów szkoleniowych, w zależności od wariantu dostajesz wsparcie mentora, który wypycha Cię z wieczorowo-netflixowej strefy komfortu lub trafiasz do klasy (zazwyczaj wirtualnej), gdzie wraz z innymi kursantami poznajesz nowe zagadnienia i wykonujesz zadania. Czyli kurs + stałe bodźcowanie do pracy własnej.
  • zgłaszasz się do firmy, która posiada akademię, czyli taką szkółkę dla przyszłych pracowników. Tam zazwyczaj też płacisz, ale masz większą pewność znalezienia zatrudnienia po zakończeniu szkolenia – firmie zależy na wyszkoleniu nowych pracowników, bootcampom zależy na przemiale kolejnych osób. Przykładem firmy pomagającej w przebranżowieniu jest FlexDev Academy.

W moim odczuciu wszystkie powyższe opcje wymagają pracy własnej. Właściwie to mnóstwa pracy własnej. I jakoś tak się składa, że większość ludzi potrzebuje silnego bodźcca do wzmożonego wysiłku. Zapłacanie > 10 000 zł za kurs w ramach bootcampu to właśnie ten bodziec. Wydanie takiej kwoty spowoduje, że nie przerwiesz kursu po napotkaniu pierwszych trudności, tylko będziesz starać się dotrwać do końca. Co w przypakdu darmowego szkolenia na YT jest rzadzkością.

Wiem jak trudno jest się zmotywować w tzw. czasie wolnym, jak w ogóle wygospodarować ten czas. Wracasz po pracy styrany, jesteś już na ‘rodzinnym’ etapie życia, czasy studenckie minęły bezpowrotnie. Dlatego będę dalej próbował pokazać Ci, że programować możesz w ramach Twojego obecnego miejsca zatrudnienia, że możesz stopniowo zdobywać nową wiedzę z zyskiem dla siebie i Twojego obecnego pracodawcy. Do usłyszenia w kolejnych wpisach!

Categories
C#

Czy ufasz programom?

Wpis [part not set] z 8 w serii C# - podstawy

Zaufanie to złożone zagadnienie. W zależności od naszych doświadczeń nasz poziom zaufania do ludzi jest różny. Jeżeli w przeszłości nieprzyjemnie sparzyliśmy się względem drugiej osoby to trudno nam przyjdzie jej zaufać. Z drugiej strony, w przypadku solidnej współpracy łatwiej nam przyjdzie powierzyć innemu człowiekowi nasze tajemnice czy dobra materialne.

I choć daleki jestem od personifikowania oprogramowania to dostrzegam tutaj kilka podobieństw. Odpowiedz proszę na pytanie: jak często aktualizujesz programy na Twoim komputerze? Czujesz, na ekranie pojawia się nowe okno Dostępna jest aktualizacja, stajesz przed wyborem: Aktualizuj lub Odłóż na później. Którą pigułkę wybierzesz? Jeżeli w przeszłości miałeś problemy po lub w trakcie aktualizacji to nie będziesz skory do ponownego wskoczenia do tej samej rzeki – będziesz odkładał update w nieskończoność. Przyczyną tego zachowania była utrata zaufania – poprzedni proces aktualizacji nie przebiegł tak bajkowo jak obiecywał ekran z nowościami.

A teraz podkręćmy miernik konsekwencji. Twoje biuro projektowe zakupiło nowiutki, lśniący program do MESu. Sprzedawca obiecywał, że teraz połowa dotychczasowej roboty zrobi się sama a raporty będą tak czytelne, że nawet Twoja teściowa zrozumie jak redukowane są momenty zginające nad słupami. Uruchamiasz nowy soft, poznajesz jego interfejs, modelujesz pierwsze konstrukcje. Myślisz – kurczę, faktycznie kawał dobrego programu, pracuje się aż miło – trzyma Cię jeszcze fala początkowego entuzjazmu. Wypluwasz raport, za raportem, koledzy z biurek obok patrzą z niedowierzaniem jak bardzo (znów) jarasz się pracą. Jeden z nich, najbardziej doświadczony podchodzi, patrzy na przebieg wykresu sił wewnętrznych w belce i kiwa z niedowierzaniem głową. Odkrył błąd, gruby błąd w Twojej najnowszej zabawce, który spowodowałby nadmierne zarysowania konstrukcji i uruchomiłby ulubioną grę budowlańców: spier*lo-pong, czyli wzajemne przerzucanie się odpowiedzialnością kto zawinił. W tym wypadku, winnym byłbyś Ty – za bardzo zaufałeś nowemu programowi, którego producent w warunkach użytkowania zastrzegł, że całą odpowiedzialność zrzuca na Ciebie, bo jesteś przecież doświadczonym inżynierem.

Trochę przejaskrawiłem, wiem, ale potrzebowałem tej historii żeby wskazać, że błędy w oprogramowaniu mogą być bardzo kosztowne i zazwyczaj to użytkownik ponosi ich konsekwencje. Błędy, czyli bugi towarzyszą rozwojowi softu od pierwszych dni i pozostają z nim, większe lub mniejsze, przez cały czas. Programowanie to złożony proces, to wykuwanie nowej, wirtualnej materii przy pomocy potęgi ludzkiego umysłu i sprawności palcy. Czasami, gdzieś pomiędzy kolejnymi uderzeniami klawiszy do kodu źródłowego wkradną się błędy .

Bugów nie unikniemy mówiąc pracuj uważniej, skup się, przestań myśleć o porannej walce o założenie czapki przez Twoje dziecko. One były, są i będą – lepiej zaakceptujmy ten fakt i zastanówmy się jak minimalizować ich częstotliwość, jak szybko je wykrywać.

Spójrz na poniższą metodę. Czy potrafisz na podstawie tego screena powiedzieć jaka będzie wartość Fctm dla klasy betonu C40/50?

Ja, jako autor, również z marszu Ci nie powiem, muszę sie zastanowić, przeanalizować flow działania. Program składa się z setek, tysięcy, milionów podobnych metod, z których każda wykonuje inne czynności. Szukanie błędów metodą organoleptyczną, scrollując po kodzie to ostateczność, często konieczna, ale w ostatniej fazie, kiedy już wiemy, że gdzieś tam czai się błąd. Tylko skąd wiemy, że program posiada błędy?

Możemy poczekać na maile/telefony od zdenerwowanych użytkowników, którzy napotkają na bugi. To jazda po bandzie, zwłaszcza w przypadku płatnego oprogramowania, gdzie użytkownik oczekuje jakości za wydane pieniądze. Nie traktujmy ich jako testerów.

Właśnie, testerzy! Może by tak zatrudnić nowych ludzi do testowania albo poprosić kolegę obok żeby między narysowaniem szachtu a zamodelowaniem biegu schodowego sprawdził nasz program. Na pierwsze nie ma pieniędzy, na drugie nie ma czasu. Przynajmniej w klasycznym środowisku budowlanym.

Potrzebujemy innego rozwiązania, czegoś co dawałoby poczucie pewności i niezawodności tworzonego przez nas oprogramowania. Czegoś, co samo testowałoby program, co sygnalizowałoby błędy zanim zauważy je użytkownik. Potrzebujemy programu testującego inny program – potrzebujemy testów jednostkowych.

Testy jednostkowe to specyficzny rodzaj testowania aplikacji. Kontroli poddawane są bardzo małe fragmenty kodu, zazwyczaj pojedyncze metody. Skoro średniej wielkości program składa się z tysięcy metod – testów też muszą być tysiące. Obawiasz się pewnie, że wykonanie takiej liczby testów to długie godziny żmudnej procedury. Uspokoję Cię, że testy jednostkowe mają ukierunkowane działanie, są szybkie przez co ich wykonanie to ułamki sekundy. Dlatego całkiem zgrabnie skaluja się wraz z rozwojem aplikacji.

Testy spełniają jedną podstawową rolę – dają programiście możliwość szybkiego sprawdzenia, że jego nowe zmiany nie zepsuły poprzedniej funkcjonalności programu. Pracując nad dużą aplikacją w wieloosobowym zespole nie sposób znać wszystkich smaczków i niuansów nagromadzonych w wyniku wielu miesięcy/lat pracy. Testy jednostkowe dają Ci spokój ducha i pozwalają sprawdzić przykazanie programisty: po pierwsze nie spier* tego co już działa.

Oczywiście testowanie odizolowanych fragmentów aplikacji z użyciem testów jednostkowych nie gwarantuję, że po zebraniu ich w większe bloki funkcyjne dalej wszystko będzie działało. Istnieją różne strategie testowania, na potrzeby tego wpisu skupię się wyłącznie na testach jednostkowych, bo chciałbym sprawdzić czy uzyskuję poprawne obiekty naszej nowej klasy Concrete.

Pierwszym krokiem jest dodanie nowego projektu do naszej solucji. Tym razem w polu wyszukiwania wpisz NUnit i wybierz wersję dla .NET Framework. NUnit to jedna z możliwych platform testowych, czyli zestaw narzędzi, które umożliwiają sprawne tworzenie i wykonywanie testów.

Standardowa konwencja nazywania projektów z testami to: <NazwaTestowanegoProjektu>.Tests, dlatego ja użyję nazwy ConcreteLibrary.Tests. Po utworzeniu projektu zmienię jeszcze nazwę początkowego pliku .cs w ramach nowego projektu na ConcreteTest – bo to właśnie klasę Concrete będę chciał przetestować.

Czas na dodanie zależności pomiędzy naszym nowym projektem a tym co już było. W tym celu kliknę PPM na references i wskażę w nowym oknie ConcreteLibrary.

Warto jeszcze zadokować nowe okno w Visual Studio, które umożliwi szybki podgląd wyników testowania. Włącz Test Explorer’a, a następnie przeciągnij go w obszar, gdzie dotychczas widziałeś Twoje projekty w formie drzewa (Solution Explorer):

Ok, pora na pierwszy test. Zacznę od negatywnego scenariusza, czyli próby stworzenia obiektu Concrete dla nieobsługiwanej klasy betonu, czyli czegoś co nie zaczyna się od litery C i nie posiada w swojej nazwie dokładnie jednego znaku /. Niech na początku będzie to pusty string. Próba utworzenia obiektu w tej sytuacji powinna zakończyć się rzucenie wyjątku (błędu w trakcie wykonywania). Sprawdźmy czy tak się stanie.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace ConcreteLibrary.Tests
{
    [TestClass]
    public class ConcreteTest
    {
        [TestMethod]
        public void Empty_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(""));
        }
    }
}

Nazewnictwo testów to przedmiot sporów i dyskusji akademików i praktyków. Ja preferuję nazewnictwo ‘naturalne’, czyli zapis prawie jak zdanie, które oddaję sens tego testu. Nazewnictwo testów jest kluczowe kiedy któryś z testów przestaje przechodzić – zwraca błąd. Przy jasnych i klarownych nazwach wystarczy wtedy rzut oka na nazwę testu żeby mieć pojęcie co poszło nie tak.

Ok, mamy zadeklarowany test, warto by sprawdzić czy on przechodzi. W tym celu przejdź do nowo dodanej zakładki Test Explorer i wybierz przycisk Run. Po chwili otrzymasz rezultat Twojego testu – zielono, znaczy jest dobrze.

Patrząc na nasz pierwszy test coś przyszło mi do głowy. Co się stanie jeżeli ktoś spróbuje utworzyć nowy obiekt klasy Concrete nie podając nic, przekazując do konstruktora null? No właśnie, sprawdźmy to pisząc nowy test i wykonajmy go.

Pięknie – program się wysypał! Ha, udało się w porę opatrzyć to zachowanie testem i sprawdzić zachowanie w kontrolowanych warunkach. To oznacza, że nasz konstruktor nie wie co zrobić w tej sytuacji. Zmieńmy to zachowanie modyfikując go w źródłowym projekcie ConcreteLibrary:

        public Concrete(string name)
        {
            if(string.IsNullOrEmpty(name))
                throw new ArgumentNullException(nameof(Name), "Provided concrete class name is not allowed");

            Name = name;

            InitalizeConcreteProperties();
        }

Nowością są linijki 3 i 4 w powyższym listingu. Sprawdzają one czy przekazany tekst w ogóle istnieje i czy przypadkiem nie jest pusty. Jeżeli taka sytuacja zaistnieje to zwrócony zostanie wyjątek.

Zmieniliśmy to co wychwycił test – sprawdźmy czy nasza zmiana zadziała i (co najważniejsze) czy nie zepsuliśmy tego co już działało. Po uruchomieniu wszystkich testów widzę już tylko kolor zielony – jest dobrze 🙂

Dodajmy jeszcze co najmniej dwa testy sprawdzające wartości powstające w trakcie inicjalizacji betonu. Dla Fctm mieliśmy instrukcję warunkową, która w zależności od wartości wytrzymałości na ściskanie wyznaczała kolejny parametr z różnych wzorów – sprawdźmy czy to działa. Tylko najpierw musimy do naszego testowego projektu dodać odwołanie do paczki nugetowej UnitsNet – tej od zamiany jednostek. Zrób to proszę identycznie jak w poprzednim wpisie.

Zestaw czterech testów sprawdzających podstawowe zachowanie konstruktora może wyglądać tak:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace ConcreteLibrary.Tests
{
    [TestClass]
    public class ConcreteTest
    {
        [TestMethod]
        public void Empty_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(""));
        }

        [TestMethod]
        public void Null_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(null));
        }

        [TestMethod]
        public void Check_concrete_parameters_when_less_than_50_MPa()
        {
            Concrete concrete = new Concrete("C30/37");

            double delta = 0.01;
            Assert.AreEqual(concrete.Fck.Megapascals, 30, delta);
            Assert.AreEqual(concrete.Fck_cube.Megapascals, 37, delta);
            Assert.AreEqual(concrete.Fcm.Megapascals, 38, delta);
            Assert.AreEqual(concrete.Fctm.Megapascals, 2.90, delta);
            Assert.AreEqual(concrete.ElasticModulus.Gigapascals, 32.837, delta);
        }

        [TestMethod]
        public void Check_concrete_parameters_when_greater_than_50_MPa()
        {
            Concrete concrete = new Concrete("C55/67");

            double delta = 0.01;
            Assert.AreEqual(concrete.Fck.Megapascals, 55, delta);
            Assert.AreEqual(concrete.Fck_cube.Megapascals, 67, delta);
            Assert.AreEqual(concrete.Fcm.Megapascals, 63, delta);
            Assert.AreEqual(concrete.Fctm.Megapascals, 4.21, delta);
            Assert.AreEqual(concrete.ElasticModulus.Gigapascals, 38.214, delta);
        }
    }
}

Czy cztery testy wystarczą? To zależy 😉 Wraz z pisaniem kolejnych metod w ramach klasy beton powinniśmy dodawać coraz to nowe testy, obejmujące nowe funkcjonalności. W ten sposób niejako jednocześnie rozwijamy naszą aplikację i naszego prywatnego testera, który będzie na nasze zawołanie sprawdzał efekty naszej pracy – przyznasz, że to kusząca perspektywa.

Categories
C#

Węzeł betoniarski

Wpis [part not set] z 8 w serii C# - podstawy

(dźwięk wybierania połączenia)
– Halo – słychać znudzony głos operatora
– Dzień dobry, w przyszłym tygodniu będziemy wylewać przyczółek wiaduktu na tej nowej obwodnicy, będziemy potrzebować tak z 80 m3 betonu – mówi rozentuzjazmowany praktykant
– Dobrze, a jakiego betonu potrzebujecie?
– To będzie mieszanka o fck = 40, fcm = 48, fctk,0,05 =2.5, Ecm = 35 …
– Panie, ocipiał Pan? Powiedz Pan jaka ma być klasa betonu.
– Aaa, o to chodzi. C40/50 … ale to Panu wystarczy?
– Mieszam te kamyki z cementem od 32 lat, wiem co to znaczy C40/50. Jestem jak fabryka. Niech Pan poda dokładny adres dostawy. A i połowa płatności z góry …

W poprzednim poście stworzyliśmy klasę Concrete, odpowiadającą inżynierskiemu pojęciu klasa betonu.

    public class Concrete
    {
        public string Name { get; set; }
        public double ElasticModulus { get; set; }

        public Concrete(string name, double elasticModulus)
        {
            Name = name;
            ElasticModulus = elasticModulus;
        }

        public double CalculateEffectiveElasticModulus(double creepCoefficient)
        {
            return ElasticModulus / (1 + creepCoefficient);
        }
    }

Konstruktor tej klasy wymaga podania nazwy klasy betonu oraz modułu Younga. Tylko, że w 99% przypadków wartość modułu Younga jest skorelowana jeden-do-jeden z nazwą klasy betonu. Podobnie kolejne parametry opisujące zachowanie fizyczne betonu – wytrzymałość na ściskanie, rozciąganie, itd. Rozwijając nasz program o kolejne funkcjonalności wymagające sprecyzowania parametrów betonu szybko doszlibyśmy do konstruktora wymagającego >10 różnych wartości. Tworzenie takich obiektów (czysto ludzko) jest koszmarem – bardzo łatwo jest pomylić kolejnosć argumentów. A i tak te wszystkie parametry wynikają z zadanej klasy betonu – dokładnie jak w rozmowie z operatorem węzła betoniarskiego.

Dlatego proponuję wykorzystać inżynierskie zależności z Eurokodu i zmodyfikować naszą klasę Concrete. Spójrz co naklepałem:

    public class Concrete
    {
        public string Name { get; private set; }
        public double Fck { get; private set; }
        public double Fck_cube { get; private set; }
        public double Fcm { get; private set; }
        public double Fctm { get; private set; }
        public double ElasticModulus { get; private set; }

        public Concrete(string name)
        {
            Name = name;

            InitalizeConcreteProperties();
        }

        private void InitalizeConcreteProperties()
        {
            switch (Name)
            {
                case "C12/15":
                    Fck = 12 * Math.Pow(10, 6);
                    Fck_cube = 15 * Math.Pow(10, 6);
                    break;
                default:
                    throw new ArgumentNullException(nameof(Name), "Provided concrete class name is not allowed");
            }

            Fcm = Fck + 8 * Math.Pow(10, 6);

            Fctm = (Fck <= 50 * Math.Pow(10, 6)) ?
                0.30 * Math.Pow(Fck * Math.Pow(10, -6), 2 / 3.0) :
                2.12 * Math.Log(1 + Fcm * Math.Pow(10, -6) / 10);

            ElasticModulus = 22 * Math.Pow((Fcm * Math.Pow(10, -6)) / 10, 0.3) * Math.Pow(10, 9);
        }

        //https://chodor-projekt.net/encyclopedia/pelzanie-skurcz-betonu/#(33)
        public double CalculateEffectiveElasticModulus(double creepCoefficient)
        {
            return ElasticModulus / (1 + creepCoefficient);
        }
    }

Co najbardziej rzuca Ci się w oczy? Mnie strasznie kłują te mnożenia przez Math.Pow(10, 6) i odwrotne Math.Pow(10, -6). Normowe wzory w większości wymagają podawania wartości w określonych jednostkach – nie wystarczy rzucić im wartości w paskalach. Czasami wzór został tak skonstruowany, że tylko podanie wartości w MPa da oczekiwany rezultat.

Tak było, jest i będzie – musimy się dostosować. Klasycznym rozwiązaniem byłoby przyjęcie, że operować będziemy na układzie SI i każdorazowo będziemy zamieniać podstawowe wartości na ich wersje z kilo/mega/giga – patrz przykład powyżej. Do tego piękny opis poszczególnych argumentów metod połączony ze wskazaniem oczekiwanej jednostki i mamy czyste ręce. Tylko, że prędzej czy później pomylimy się, podamy gdzieś formę w Pa zamiast w MPa, metody zaczną zwracać bzdurne wyniki. Samo patrzenie na taki kod powoduje, że podskórnie czujesz, że gdzieś tam czai się babol. Aż chciałoby się mieć magiczną różdżkę, która będzie na zawołanie dokonywać konwersji pomiędzy poszczególnymi jednostkami.

Zacznijmy więc tworzyć odpowiednie klasy potrafiące zamieniać … Wait, daj sobie na wstrzymanie. Zanim zaczniesz klepać nową funkcjonalność spójrz z boku na swój problem i zadaj fundamentalne pytanie: czy to ja jestem taki wyjątkowy czy jednak z tym zagadnieniem ktoś wcześniej się zetknął i może go rozwiązął? Odpowiedzi będą różne, ale warto pytać, zwłaszcza samego siebie.

Problem konwersji jednostek jest powszechnie znany, a co najlepsze – dostępne jest rozwiązanie, które weźmiemy z półki i wstrzykniemy do naszego programu. Przy kasie zobaczymy kwotę 0 zł – skorzystamy z biblioteki na licencji MIT, która umożliwia, wspiera dalsze wykorzystanie efektu pracy innych programistów. Pora poznać UnitsNet.

Ok, link wygląda na pierwszy rzut oka strasznie. Nie musisz analizować jak zbudowana jest ta biblioteka, jak ją skompilować – chcemy ją szybko i bezboleśnie wykorzystać w naszym programie. W tym celu naciśnij PPM na References i wybierz Manage NuGet Packages.

Witaj w królestwie paczek, które czekają na skosztowanie w Twoich kolejnych projektach. Chcemy spróbować paczki UnitsNet, więc wpisz proszę tę nazwę w polu wyszukiwania będąc w zakładce Browse. Po wybraniu paczki wybierz Install dostępne po prawej stronie.

Po zakończeniu procesu instalacji paczka jest gotowa do używania. W tym celu musimy rozszerzyć zakres poszukiwania klas dodając nową przestrzeń nazw, w tym wypadku using UnitsNet; Spójrz jak teraz możemy zapisać szkielet klasy Concrete:

using System;
using UnitsNet;

namespace ConcreteLibrary
{
    public class Concrete
    {
        public string Name { get; private set; }
        public Pressure Fck { get; private set; }
        public Pressure Fck_cube { get; private set; }
        public Pressure Fcm { get; private set; }
        public Pressure Fctm { get; private set; }
        public Pressure ElasticModulus { get; private set; }

        public Concrete(string name)
        {
            Name = name;

            InitalizeConcreteProperties();
        }

        private void InitalizeConcreteProperties()
        {
            switch (Name)
            {
                case "C12/15":
                    Fck = Pressure.FromMegapascals(12);
                    Fck_cube = Pressure.FromMegapascals(15);
                    break;
                default:
                    throw new ArgumentNullException(nameof(Name), "Provided concrete class name is not allowed");
            }

            Fcm = Fck + Pressure.FromMegapascals(8);

            Fctm = Pressure.FromMegapascals(
                (Fck.Megapascals <= 50) ?
                0.30 * Math.Pow(Fck.Megapascals, 2 / 3.0) :
                2.12 * Math.Log(1 + Fcm.Megapascals / 10));

            ElasticModulus = Pressure.FromGigapascals(22 * Math.Pow(Fcm.Megapascals / 10, 0.3));
        }

        //https://chodor-projekt.net/encyclopedia/pelzanie-skurcz-betonu/#(33)
        public double CalculateEffectiveElasticModulus(double creepCoefficient)
        {
            return ElasticModulus.Pascals / (1 + creepCoefficient);
        }
    }
}

Zniknęły magiczne zamiany z Pa na MPa i na odwrót. Zamiast mnożenia przez wielokrotności 10-ciu mamy zgrabną definicję:

Pressure Fck = Pressure.FromMegapascals(12);

Patrząc na to od razu czujesz co się dzieje w danej linijce. Podobnie jak wskazanie w jakiej jednostce chcesz uzyskać wartość, o patrz:

double valueInMegapascals = Fck.Megapascals;

Życie stało się zdecydowanie prostsze. Jeszcze jeżeli dodamy odczytywanie podstawowych wytrzymałości na ściskanie na bazie nazwy podanej przez użytkownika to całość klasy betonu zamknie się w poniższym kodzie:

using System;
using System.Linq;
using UnitsNet;

namespace ConcreteLibrary
{
    public class Concrete
    {
        public string Name { get; private set; }
        public Pressure Fck { get; private set; }
        public Pressure Fck_cube { get; private set; }
        public Pressure Fcm { get; private set; }
        public Pressure Fctm { get; private set; }
        public Pressure ElasticModulus { get; private set; }

        public Concrete(string name)
        {
            Name = name;

            InitalizeConcreteProperties();
        }

        private void InitalizeConcreteProperties()
        {
            bool initialized = false;
            if (Name.StartsWith("C") && Name.Count(x => x == '/') == 1)
            {
                string[] splitted = Name.Split('/');
                int fckFromName;
                int fckCubeFromName;

                if (int.TryParse(splitted[0].Substring(1), out fckFromName) &&
                    int.TryParse(splitted[1], out fckCubeFromName))
                {
                    Fck = Pressure.FromMegapascals(fckFromName);
                    Fck_cube = Pressure.FromMegapascals(fckCubeFromName);

                    initialized = true;
                }
            }

            if (!initialized)
            {
                throw new ArgumentNullException(nameof(Name), "Provided concrete class name is not allowed");
            }

            Fcm = Fck + Pressure.FromMegapascals(8);

            Fctm = Pressure.FromMegapascals(
                (Fck.Megapascals <= 50) ?
                0.30 * Math.Pow(Fck.Megapascals, 2.0 / 3.0) :
                2.12 * Math.Log(1 + Fcm.Megapascals / 10.0));

            ElasticModulus = Pressure.FromGigapascals(22 * Math.Pow(Fcm.Megapascals / 10.0, 0.3));
        }

        //https://chodor-projekt.net/encyclopedia/pelzanie-skurcz-betonu/#(33)
        public double CalculateEffectiveElasticModulus(double creepCoefficient)
        {
            return ElasticModulus.Pascals / (1 + creepCoefficient);
        }
    }
}

Linijki od 26 do 40 to zamiana nazwy klasy betonu na dwie podstawowe charakterystyki, które później przy pomocy normowych zależności zamieniane są na pozostałe zmienne opisujące nasz beton.

Z tego posta chcę żebyś zapamiętał jedną podstawową różnicę pomiędzy światem inżynierów a programistów – w tym drugim dzielenie się wiedzą jest standardem, również w postaci udostępniania za darmo wartościowych paczek, z których możesz skorzystać. Wzajemna pomoc nakręca rozwój – dopóki do gry nie włączy się dział finansów, ale to opowieść na inną historię.

Categories
C#

Moment bezwładności przekroju niezarysowanego

Wpis [part not set] z 8 w serii C# - podstawy

Poznaliśmy koncept klas i obiektów, udało nam się dotychczas wyróżnić dwie klasy: ConcreteSection oraz Rebar. Jednak poza definicją klasy Rebar nie zaliczyliśmy znacznego postępu. Naszym celem jest stworzenie programu wyznaczającego moment bezwładności prostokątnego przekroju niezarysowanego. Podstawy teoretyczne pochodzą ze strony https://chodor-projekt.net/ – ogromne podziękowania dla Pana Leszka Chodora za dzielenie się wiedzą!

Nasze zadanie zahacza o pojęcie ‘sprowadzonego pola przekroju’. Mamy betonowy przekrój prostokątny w którym znajdują się pręty stalowe, wykonane z innego materiału, posiadające znacznie większy moduł spreżystości niż otaczający je beton. Faza niezarysowana jest o tyle przyjemna, że możemy przymknąc oko na mikroświat i przyjąć w trakcie obliczeń, że miejsce zajmowane przez stal jest odpowiednio sztywniejsze od betonu, do którego chcemy ją sprowadzić. W tym celu będziemy potrzebować współczynnika ‘zamieniającego’ stal w beton.

    \[ \alpha_e=\frac{E_s}{E_{c,eff}}\]

Powyższa czarodziejska różdżka potrzebuje do działania modułu sprężystości stali i efektywnego modułu sprężystości betonu. Hmm, czyli nie wystarczy nam wiedza jaką nazwę nosi zastosowana klasa betonu i stali – potrzebujemy większej liczby właściwości opisujących te ‘byty’. Moment, w którym okazuje się, że do opisania danej cechy nie wystarczy jedna liczba lub słowo to czas na wydzielenie nowej klasy.

Tylko zanim się rozpędzimy wykonajmy jeszcze jedną czynność porządkową – utwórzmy nowy projekt, który będzie biblioteką klas, czyli czymś, czego nie da się samodzielnie uruchomić poprzez plik .exe ale będzie w przyszłości stanowić część większych programów. Z poziomu menu File wybierz Add -> New Project a następnie wskaż nowy typ projektu Class Library (.NET Framework). Nazwijmy go ConcreteLibrary.

Utworzenie nowego projektu i przekazanie mu odpowiedzialności za nową, samodzielną funkcjonalność jest ważną czynnością porządkową – w naszym wypadku umożliwi to sprawne wykorzystanie algorytmu w połączeniu z programami budowlanymi.

W ramach projektu ConcreteLibrary utwórz nowe klasy materiałowe: Steel oraz Concrete. Na użytek tego zadania proponuję coś na kształt:

    public class Steel
    {
        public string Name { get; set; }
        public double ElasticModulus { get; set; }

        public Steel(string name, double elasticModulus)
        {
            Name = name;
            ElasticModulus = elasticModulus;
        }
    }
    public class Concrete
    {
        public string Name { get; set; }
        public double ElasticModulus { get; set; }

        public Concrete(string name, double elasticModulus)
        {
            Name = name;
            ElasticModulus = elasticModulus;
        }

        public double CalculateEffectiveElasticModulus(double creepCoefficient)
        {
            return ElasticModulus / (1 + creepCoefficient);
        }
    }

Klasa Concrete jest w stanie obliczyć wartość efektywnego modułu sprężystości, uwzględniając wskazany współczynnik pełzania.

Mając tak zdefiniowane podstawowe klasy materiałowe musimy zmodyfikować dotychczasowe ConcreteSection oraz Rebar tak, żeby korzystały z dobrodziejstw nowych klas (początkowo przechowywaliśy tylko nazwy użytych materiałów):

    public class ConcreteSection
    {
        public double Width { get; set; }
        public double Height { get; set; }
        public Concrete ConcreteClass { get; set; }

        public ConcreteSection(double width, double height, Concrete concreteClass)
        {
            Width = width;
            Height = height;
            ConcreteClass = concreteClass;
        }

        public double CalculateArea()
        {
            return Width * Height;
        }

        public double CalculateFirstMomentOfArea()
        {
            return CalculateArea() * Height / 2.0;
        }

        public double CalculateMomentOfInertia()
        {
            return (Width * Math.Pow(Height, 3)) / 12;
        }
    }

Klasa Rebar zaprezentowana poniżej zyskała dodatkową moc, potrafi teraz wyznaczyć odległość do górnej krawędzi przekroju. Konstruktor klasy oczekuje podania odległości od dolnej krawędzi, z kolei żelbetowe algorytmy wykorzystują czasami domiar od góry.

public class Rebar
{
    public double Diameter { get; set; }
    public Steel SteelClass { get; set; }
    public double DistanceFromBottomEdge { get; set; }

    public Rebar(double diameter, Steel steelClass, double distanceFromBottomEdge)
    {
        Diameter = diameter;
        SteelClass = steelClass;
        DistanceFromBottomEdge = distanceFromBottomEdge;
    }

    public double CalculateArea()
    {
        return Math.PI * Diameter * Diameter / 4.0;
    }

    public double CalculateDistanceFromTopEdge(double sectionHeight)
    {
        return sectionHeight - DistanceFromBottomEdge;
    }
}

Przekrój żelbetowy składa się z wielu prętów – obiektów klasy Rebar. Musimy zastanowić się w jaki sposób będziemy przechowywać o nich informacje. Istnieją dwie podstawowe możliwości:

  • zbierać pręty w ramach klasy ReinforcedSection, która będzie również finalnie obliczać moment bezwładności,
  • stworzyć nową klasę, której głównym zadaniem będzie agregowanie informacji o prętach w przekroju.

Innymi słowami: w przyszłości wielokrotnie będziesz stawał przed dylematem gdzie umiejscawiać nowe funkcjonalności – upychać je w istniejącej strukturze klas czy zatrzymać się na chwilę, wydzielić nowe klasy oraz powiązania pomiędzy nimi. Jeżeli goni Cię deadline, to zdecydujesz się na zwiększanie już posiadanych klas – będzie szybciej. Ucierpi na tym zarządzalność projektem w przyszłości – trudniej będzie dodać nowości, trudniej będzie zrozumieć powody błędnego działania. Ale … dostarczyłeś nową funkcjonalność na czas – ot, klasyczny trade-off programisty.

Mamy czas, stwórzmy zatem nową klasę: RebarsInSection:

public class RebarsInSection : IEnumerable<Rebar>
    {
        private List<Rebar> _rebars = new List<Rebar>();

        public Rebar this[int index]
        {
            get { return _rebars[index]; }
            set { _rebars.Insert(index, value); }
        }

        public Steel GetSteelClass()
        {
            if (_rebars.Count > 0)
                return _rebars.First().SteelClass;
            else
                return null;
        }

        public void AddRebar(Rebar rebar)
        {
            _rebars.Add(rebar);
        }

        public double CalculateArea()
        {
            return _rebars.Sum(x => x.CalculateArea());
        }

        public IEnumerator<Rebar> GetEnumerator()
        {
            return _rebars.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _rebars.GetEnumerator();
        }
    }

Powyższa klasa zawiera zupełnie nowy element – po podaniu jej nazwy wpisałem : IEnumerable<Rebar>. To deklaracja implementacji interfejsu, czyli rodzaju umowy pomiędzy róznymi klasami gwarantujących sprawną współpracę. IEnumerable to podstawowy interfejs umożliwiający wyliczanie zgrupowanych obiektów – w naszym wypadku prętów. Metody GetEnumerator() musieliśmy zaimplementować w związku z wykorzystaniem tego interfejsu – niewielki koszt biorąc pod uwagę płynącego z tego zyski, o nich za chwilę.

Klasa RebarsInSection zawiera pole _rebar będące listą prętów. To w niej będziemy przechowywać pręty w przekroju. Dodatkowo mamy tu kilka metod pomocniczych, które przydadzą się w naszej finalnej klasie: ReinforcedSection, która połączy wszystkie fragmenty układanki.

    public class ReinforcedSection
    {
        private ConcreteSection _concreteSection;
        private RebarsInSection _rebars;

        public ReinforcedSection(ConcreteSection concreteSection, RebarsInSection rebars)
        {
            _concreteSection = concreteSection;
            _rebars = rebars;
        }

        public double CalculateMomentOfInertia()
        {
            double cogFromTopEdge = CalculateCenterOfGravityFromTopEdge();
            double steelToConcreteCoefficient = CalculateSteelToConcreteCoefficient();

            double inertia = _concreteSection.CalculateMomentOfInertia();
            inertia += _concreteSection.CalculateArea() * Math.Pow(_concreteSection.Height / 2 - cogFromTopEdge, 2);

            foreach (Rebar rebar in _rebars)
            {
                double distanceToCog = rebar.CalculateDistanceFromTopEdge(_concreteSection.Height) - cogFromTopEdge;
                inertia += rebar.CalculateArea() * steelToConcreteCoefficient * Math.Pow(distanceToCog, 2);
            }

            return inertia;
        }

        private double CalculateCreepCoefficient()
        {
            return 1.5;
        }

        private double CalculateSteelToConcreteCoefficient()
        {
            if (_rebars.Count() == 0)
                return 1;

            double creepCoefficient = CalculateCreepCoefficient();
            double concreteEffectiveModulus = _concreteSection.ConcreteClass.CalculateEffectiveElasticModulus(creepCoefficient);

            double steelModulus = _rebars.GetSteelClass().ElasticModulus;

            return steelModulus / concreteEffectiveModulus;
        }

        private double CalculateAreaOfConcreteEquivalentSection()
        {
            double area = _concreteSection.CalculateArea();
            area += _rebars.CalculateArea() * CalculateSteelToConcreteCoefficient();

            return area;
        }

        private double CalculateFirstMomentOfAreaFromTopEdge()
        {
            var firstMomentOfArea = _concreteSection.CalculateFirstMomentOfArea();

            double steelToConcreteCoefficient = CalculateSteelToConcreteCoefficient();
            foreach (Rebar rebar in _rebars)
            {
                double distanceFromTopEdge = rebar.CalculateDistanceFromTopEdge(_concreteSection.Height);
                firstMomentOfArea += rebar.CalculateArea() * steelToConcreteCoefficient * distanceFromTopEdge;
            }

            return firstMomentOfArea;
        }

        private double CalculateCenterOfGravityFromTopEdge()
        {
            return CalculateFirstMomentOfAreaFromTopEdge() / CalculateAreaOfConcreteEquivalentSection();
        }
    }

Ok, tu zaczyna się coś dziać, przeanalizujmy to od góry:

  • dwa pola: _concreteSection oraz _rebars inicjalizowane w konstruktorze,
  • publiczna metoda CalculateMomentOfInertia(), która dyryguje pomniejszymi metodami
  • zbiór metod prywatnych, czyli takich do których dostęp jest tylko w ramach klasy, które nie są dostępne z poziomu innych klas.

Na użytek tej części zdecydowałem się na uproszczenie – metoda CalculateCreepCoefficient() zwraca zawsze wartość 1.5. Nie chciałem dalej rozbudowywać programu uwzględniająć różne sytuacje, czasy i zależności od wymiaru charakterystycznego przekroju – przyjdzie na to czas.

Wydzielenie do nowego projektu pełnej funkcjonalności rodzi pytanie w jaki sposób wykorzystać ją w innych projektach. Programowanie bardzo często polega na sklejaniu różnych bibliotek w celu uzyskania nowego produktu. W naszym wypadku do dyspozycji mamy projekt/bibliotekę ConcreteLibrary. Wykorzystanie jej w początkowym projekcie PierwszaKonsola musimy zaczać od dodania odpowiedniej referencji. Prawym przyciskiem myszy kilknij na References w drzewie projektu do którego chcesz dodać nowe odniesienie.

W nowym oknie dialogowym ‘zaptaszkuj’ wzorcowy projekt, który chcesz wykorzystać:

Ostatnim krokiem do wykorzystania gotowej funkcjonalności jest wskazanie jakie przestrzenie nazw będa wykorzystywane w ramach danego pliku źródłowego. W pliku Program.cs, który jest punktem wejście w projekcie PierwszaKonsola musimy na samej górze dodać using ConcreteLibrary; , co podpowie kompilatorowi jakiej przestrzeni nazw będziemy używać. Kompletna wersja Program.cs przedstawiająca jak korzystać z biblioteki w innym projekcie znajduje się poniżej:

using System;
using ConcreteLibrary;

namespace PierwszaKonsola
{
    public class Program
    {
        public static void Main()
        {
            Concrete concreteClass = new Concrete("C30/37", 34 * Math.Pow(10, 9));
            Steel steelClass = new Steel("B500SP", 200 * Math.Pow(10, 9));

            double width = 0.25;
            double height = 0.50;
            ConcreteSection section = new ConcreteSection(width, height, concreteClass);

            double diameter = 0.016;
            double cover = 0.030;
            Rebar rebar = new Rebar(diameter, steelClass, cover + diameter / 2);

            RebarsInSection rebars = new RebarsInSection();
            rebars.AddRebar(rebar);

            ReinforcedSection reinforcedSection = new ReinforcedSection(section, rebars);

            string message1 = string.Format("Moment bezwładności wynosi {0:N8}", reinforcedSection.CalculateMomentOfInertia());
            Console.WriteLine(message1);
            Console.ReadLine();
        }
    }
}

Mam kilka zarzutów co do wygody stosowania tej biblioteki. Klasy betonu czy stali powinny miec możliwość automatycznego uzupełniania wartości poszczególnych parametrów – zajmiemy się tym w kolejnej części. Na dziś wystarczy nowości 🙂

Categories
C#

Wprowadźmy klasy

Wpis [part not set] z 8 w serii C# - podstawy

Nie, nie, nie – spokojnie, nie zamierzam wchodzić w tematykę społeczną, a na pewno nie w tym miejscu. Chcę za to dodać do naszego prostokątnego przekroju stalowe pręty, żebyśmy wreszcie mogli zacząć liczyć coś sensownego. W tym celu musimy porzucić dotychczasowy sposób pisania – nie sprostałby wymagań rosnącej funkcjonalności.

Jeżeli zetknąłeś się z programowaniem w ramach studiów budowlanych to prawdopodobnie klepałeś kod w myśl paradygmatu imperatywnego, gdzie program to ciąg sekwencji wykonywanych jedna po drugiej, zgodnie z założeniami programisty. Choć wątpię żebyś zastanawiał się nad istotą tej pracy – raczej chciałeś jak najszybciej skończyć zajęcia, żeby zająć się tym co wtedy wydawało się ważniejsze.

C# ma wytuatuowane na czole jestem obiektowy i choć to za słaba podpowiedź do gry ‘zgadnij kim jestem’ to warto zrozumieć na podstawowym poziomie koncept obiektowości – będziemy z niego korzystać przez cały czas. Postaram się go przedstawić na przykładzie naszego zadania – przekroju prostokątnego ze zbrojeniem.

Patrząc na jeden, konkretny przekrój w środku przęsła belki widzimy jeden prostokąt i kilka/kilkanaście prętów. W jaki sposób opisujemy te cześci składowe? Zacznijmy od przypomnienia poprzedniej części, czyli opiszmy przekrój.

Betonowy przekrój prostokątny w zakresie wyznaczania momentu bezwładności:

  • ma szerokość,
  • ma wysokość,
  • ma klasę betonu,
  • zwraca pole powierzchni,
  • zwraca moment bezwładności.

Co to za zabawy linigwistyczne zapytasz. Język programowania to właśnie język, kierujący się określoną logiką. Jak to będzie wyglądało w przypadku pręta stalowego w przekroju:

  • ma średnicę,
  • ma klasę stali,
  • ma położenie, lokalizację w ramach przekroju,
  • zwraca pole powierzchni.

Z premedytacją używałem dwóch czasowników: ma oraz zwraca.

  • Ma to podstawowa wartość, która określa, doprecyzowuje nasz element, która jest właściwością.
  • Zwraca to wyróżnik czegoś wynikowego, uzyskanego z operacji matematycznych na właściwościach lub ‘umiejętność’ danego obiektu. Zwraca to moje określenie metody, czyli funkcji, która coś robi.

Tak patrzę na betonowy przekrój oraz pręt w nim zawarty i trudno mi znaleźć cechy wspólne. Oba mogą zwrócić pole powierzchni, ale to trochę za mało, żeby wrzucić je do jednego worka – to są dwa oddzielne, logiczne byty. Fajnie by było zaimplementować nasz opis i właśnie w tym celu potrzebujemy konceptu klasy, czyli definicji jakiegoś elementu, obiektu. Spójrz na nową klasę ConcreteSection:

    public class ConcreteSection
    {
        // Properties
        public double Width { get; set; }
        public double Height { get; set; }
        public string ConcreteClass { get; set; }

        // Methods
        public double GetArea()
        {
            return Width * Height;
        }
        public double GetMomentOfInertia()
        {
            return (Width * Math.Pow(Height, 3)) / 12;
        }
    }

A jak by wyglądała klasa pręta stalowego?

    public class Rebar
    {
        // Properties
        public double Diameter { get; set; }
        public string SteelClass { get; set; }
        public double Location { get; set; }

        // Methods
        public double GetArea()
        {
            return Math.PI * Diameter * Diameter / 4.0;
        }
    }

Klasa to taki stempelek z ziemniaka określający kształt utworzonych przy jej pomocy obiektów. Definicja danej klasy jest jedna, natomiast obiektów powstałych przy jej pomocy – jeden/wiele/wedle potrzeb. Mając ziemniaczany stempelek możesz narobić tyle odbitek na ile wystarczy Ci tuszu i kartek/ścian, mając klase możesz stworzyć tyle obiektów na ile wystarczy Ci pamięci RAM w Twoim komputerze, czyli duuuuuużo.

Tylko jak stworzyć nowe odbitki z naszych pieczątek? Służy do tego słowo kluczowe new, spójrz proszę:

public static void Main()
{
    ConcreteSection section = new ConcreteSection();
    Rebar rebar = new Rebar();
}

Zastosowanie new wyraża intencję programisty – stwórz nowy obiekt danej klasy. Powyższa inicjalizacja nie jest powiązana z przypisaniem wartości do nowych obiektów – w konstruktorze klasy nie przekazujemy żadnych wartości. Mamy zwykłe ConcreteSection section = new ConcreteSection();

Konstruktor to wyjątkowa metoda/funkcja, której możemy użyć do początkowego ustawienia nowo powstałego obiektu. Na chwilę obecną nasze obiekty są golutkie, spójrz:

Brak klasy betonu, brak wysokości, brak szerokości. Próba wywołania metod obliczających pole powierzchni lub moment bezwładności zwróci zero, może to nie tragedia, ale już null’owa wartość klasy betonu może skończyć się wysypaniem programu. Nasze obiekty są zakalcowate, niezdatne do bezpiecznego spożycia. Istnieje kilka szkół inicjalizacji obiektów, my zrobimy to w bezpieczny sposób, oczekując podania wszystkich właściwości na etapie tworzenia nowych instancji klas. To dobry moment, żebyś dodał do swojego projektu nowe klasy. W tym celu naciśnij PPM na nazwie Twojego projektu widocznego w oknie po prawej stronie Visual Studio, rozwiń opcję Add i wybierz Class. Alternatywnie możesz skorzystać ze skrótu Shift + Alt + C.

W nowym oknie wpisz nazwę klasy: ConcreteSection, dodaj rozszerzenie pliku .cs i wybierz Add.

Pora na napisanie definicji klasy, proponuję żeby wyglądała następująco:

using System;

namespace PierwszaKonsola
{
    public class ConcreteSection
    { 
        // Properties
        public double Width { get; set; }
        public double Height { get; set; }
        public string ConcreteClass { get; set; }

        // Constructor
        public ConcreteSection(double width, double height, string concreteClass)
        {
            Width = width;
            Height = height;
            ConcreteClass = concreteClass;
        }

        // Methods
        public double GetArea()
        {
            return Width * Height;
        }
        public double GetMomentOfInertia()
        {
            return (Width * Math.Pow(Height, 3)) / 12;
        }
    }
}

Dodaj kolejną klasę do projektu, tym razem o nazwie Rebar:

using System;

namespace PierwszaKonsola
{
    public class Rebar
    {
        // Properties
        public double Diameter { get; set; }
        public string SteelClass { get; set; }
        public double Location { get; set; }

        // Constructor
        public Rebar(double diameter, string steelClass, double location)
        {
            Diameter = diameter;
            SteelClass = steelClass;
            Location = location;
        }

        // Methods
        public double GetArea()
        {
            return Math.PI * Diameter * Diameter / 4.0;
        }
    }
}

Mając zdefiniowane w projekcie dwie klasy zastąp proszę zawartość pliku Program.cs poniższą treścią:

using System;

namespace PierwszaKonsola
{
    public class Program
    {
        public static void Main()
        {
            ConcreteSection section = new ConcreteSection(30, 60, "C30/37");

            Console.WriteLine(section.Height);
            Console.ReadLine();
        }
    }
}

Program wyświetli w konsoli wartość 60. Może Cię kusić żeby stosować tę technikę podglądania obiektów częściej, ale nie rób tego. Zamiast wyświetlania w konsoli wartości, które Cię interesują spraw, aby program wstrzymał wykonywanie w zadanym przez Ciebie punkcie. W tym celu musisz zdefiniowac punkt debugowania, najprościej poprzez najechanie kursorem myszy na wyszarzony pasek po lewej stronie edytora:

i kliknięcie LPM – w tym momencie kropka stanie się czerwona. Uruchom teraz Twój program z poziomu VS – naciśnij F5.

W konsoli nie ma oczekiwanej wartości wysokości przekroju, co więcej focus przeszedł na Visual Studio, które mruga zachęcająco wskazując linijkę, przed którą zatrzymane zostało wykonywanie:

Właśnie wkroczyłeś w świat debugowanie, czyli podglądania w jakim stanie są obiekty w danym momencie wykonywania. To standardowe narzędzie potrzebne do zrozumienia i wyeliminowania bugów, czyli błędów w aplikacji. Zetknąłeś się z naprawdę potężną mocą, której pełne opanowanie chwilę potrwa. Na ten moment wiedz, że podstawowe opcje znajdują się na górnym pasku VS:

W obecnej sytuacji interesuje nas przede wszystkim przechodzenie do kolejnej linijki, które najłatwiej zrealizować poprzez naciśnięcie F10. Aplikacja przejdzie wtedy do kolejnej linijki kodu a my będziemy mogli sprawdzić efekty wykonania poprzedniej. Właśnie w ten sposób upewniłem się, że konstruktor poprawnie przekazał argumenty do właściwości naszego obiektu – można z niego korzystać.

Hej, pamiętasz nasze poprzednie podejście do konsoli? To w którym pobieraliśmy od użytkownika szerokość i wysokość przekroju a następnie obliczaliśmy i wyświetlaliśmy moment bezwładności. Tamto podejście łamało zasadę ograniczania odpowiedzialności mieszając w jednej klasie różne bajki – pobieranie danych od użytkownika i wyznaczanie momentu bezwładności. Spójrz na zmodyfikowany przykład wykorzystujący utworzoną dzisiaj klasę ConreteSection:

using System;

namespace PierwszaKonsola
{
    public class Program
    {
        public static void Main()
        {
            Console.WriteLine("Podaj szerokość przekroju:");
            string widthFromUser = Console.ReadLine();
            double? width = GetDoubleFromUser(widthFromUser);
            if (!width.HasValue)
                return;
            Console.WriteLine("Podaj wysokość przekroju:");
            string heightFromUser = Console.ReadLine();
            double? height = GetDoubleFromUser(heightFromUser);
            if (!height.HasValue)
                return;

            ConcreteSection section = new ConcreteSection(width.Value, height.Value, "C30/37");

            string message1 = string.Format("Moment bezwładności wynosi {0:N2}", section.GetMomentOfInertia());
            Console.WriteLine(message1);
            Console.ReadLine();
        }

        public static double? GetDoubleFromUser(string inputValue)
        {
            double result;
            if (double.TryParse(inputValue, out result))
                return result;
            else
            {
                Console.WriteLine($"Wartość {inputValue} nie może zostać zamieniona na liczbę. Koniec działania programu.");
                return null;
            }
        }
    }
}

Jest lepiej, oddelegowaliśmy wyznaczanie momentu bezwładności odpowiedniemu obiektowi zapewniając wczesniej, że będzie on posiadał komplet danych do wykonania działania (konstruktor przekazujący wartości). I tak będziemy robić w dalszych częściach – wykorzystamy naszą wiedzę domenową w celu nazwania, zdefiniowania obiektów, których potrzebujemy, określimy ich właściwości oraz umiejętności (metody) a następnie połączymy stosownie do potrzeb w elegancką aplikację. No, może czasami to będzie elegancja ciosana udarowym Bosh’em, ale hej – pamiętasz gdzie byłeś przed stworzeniem swojej pierwszej aplikacji konsolowej?

Categories
C#

Czarne jak konsola

Wpis [part not set] z 8 w serii C# - podstawy

Jeżeli po poprzednim poście męczyło Cię pytanie “po co właściwie zainstalowałem to Visual Studio?” to już piszę odpowiedź. Chociaż, w sumie, wolę żebyś sam to sprawdził – uruchom proszę na Twoim komputerze Visual Studio 2022. Po prawej stronie ekranu startowego znajdziesz przycisk Create a new project – wskaż go.

Pora na wybranie szablonu projektu. Kurczę, czego? Visual Studio oferuje Ci szereg predefiniowanych “rusztowań” projektów, które możesz zapełniać potrzebną Ci zawartością. Naszą przygodę zaczniemy od aplikacji konsolowej, czyli czegoś co spełnia poniższe cechy:

  • jest kuszące niczym Polski Ład,
  • ma rozszerzenie .exe, jest executable, czyli samowykonywalne,
  • oferuje podstawową interakcję z użytkownikiem, wyświetlając znaki na ekranie oraz przyjmując wartości od operatora,
  • standardowo ma przepiekny, głęboki, czarny kolor tła 😛

W polu wyszukiwania wpisz console app i wybierz wariant dla C# w .NET Framework.

Jeżeli dziwisz się dlaczego są dwa warianty templatu dla aplikacji konsolowej w C# to na ten moment wiedz, że wybieramy starszy sposób, z którego korzystają wszystkie programy budowlane. Jak to zwykle bywa – innowacje przybywają do nas z ‘lekkim’ opóźnieniem.

Ok, wybrałeś Next, podałeś nazwę projektu (np. PierwszaKonsola), jego lokalizację i kliknałeś w Create. Po chwili zwątpienia widzisz w pełnej krasie swój pierwszy projekt w C# – brawo Ty! Pora uruchomić program, naciśnij zachęcający, zielony grot z napisem Start.

Hmm, u Ciebie też coś czarnego pojawiło się na ekranie i szybko zniknęło? Spójrz na zawartość naszego programu, czyli obszar pomiędzy klamerkami między 12 a 13 linijką. Niczego tam nie ma i to z tego powodu nasz program bardzo szybko kończy pracę. Wpisz proszę jakieś polecenie, np. próbę wyświetlenia na ekranie tekstu, o choćby tego:

        static void Main(string[] args)
        {
            Console.WriteLine("Let's construct IT");
        }

Naciśnij znów zielony grot – chcemy sprawdzić jak teraz zadziała nasz program. Kurczę, znowu okno pojawia się i znika, a to przecież rola magika. Czegoś tu brakuje, jakiejś formy oczekiwania na interakcję użytkownika. Zmodyfikuj wnętrze metody Main, tak żeby poczekała na ruch operatora, coś na kształt tego:

    static void Main(string[] args)
    {
        Console.WriteLine("Let's construct IT");
        Console.ReadLine();
    }

Uruchom program (F5 jest skrótem dla zielonego grota, będzie szybciej) – jeeeeeest, działa! To jest moment na zawołanie żony/męża/mamy/kota i pokazanie, że potrafisz programować, a przynajmniej jesteś na dobrej drodze.

Żeby zamknąć naszą aplikację możesz użyć krzyżyka lub wykonać to, czego oczekuje linijka Console.ReadLine() czyli wpisać jakiekolwiek znaki i nacisnąć enter.

Pamiętasz kod obliczający moment bezwładności prostokąta z wcześniejszych postów? Nie? Ja też nie pamiętałem, dlatego wklejam go poniżej. Proszę skopiuj go i wklej u siebie, zastąpując to wszystko co widzisz w edytorze Visual Studio.

using System;

public class Program
{
    public static void Main()
    {
        double momentOfInertia1 = GetMomentOfInertia(30, 60);
        double momentOfInertia2 = GetMomentOfInertia(20, 50);

        string message1 = string.Format("Moment bezwładności 1 wynosi {0:N2}", momentOfInertia1);
        Console.WriteLine(message1);
        string message2 = string.Format("Moment bezwładności 2 wynosi {0:N2}", momentOfInertia2);
        Console.WriteLine(message2);

        Console.ReadLine();
    }

    public static double GetMomentOfInertia(double b, double h)
    {
        double momentOfInertia = (b * Math.Pow(h, 3)) / 12;

        return momentOfInertia;
    }
}

Uruchom nowy program, najlepiej klawiszem F5. W konsoli wyświetlone zostały dwie wartości momentu bezwładności – nasz program działa.

Gdy przedstawiałem ten przykład w Fiddle wspomniałem, że posiada on zaszyte na sztywno wartości dla których wykonane mają zostać działania. Chcemy tworzyć programy elastyczne, skrojone pod potrzeby użytkowników i właśnie teraz nadszedł czas na implementację nowej funkcji – pobierania od operatora szerokości i wysokości prostokąta.

Na samym początku metody Main dopisz na szybko linijkę Console.ReadLine(); i skieruj kursor myszy na słowo ReadLine – Visual Studio podpowie Ci co robi ta metoda oraz jaki typ zwraca. W tym przypadku jest to string, czyli słowo/zestaw znaków wpisany przez użytkownika w oknie konsoli i zatwierdzony naciśnięciem Enter.

Wykorzystajmy tę wiedzę i przypiszmy to co poda użytkownik do nowej zmiennej, żeby było intuicyjnie nazwiemy ją widthFromUser. Podobnie postąpmy z wysokością przekroju – heightFromUser.

Pobranie wartości od użytkownika i przypisanie ich do dwóch nowych zmiennych w żaden sposób nie wpłynęło na argumenty metody GetMomentOfInertia – one dalej są przypisane na sztywno. Pora to zmienić – zróbmy coś szalonego!

Wykorzystałem nowe zmienne aplikując je jako argumenty naszej metody. Skończyło się to przepiękną czerwoną falą wskazującą, że coś w tym miejscu jest nie tak. Właśnie stworzyłem coś, czego nie da się skompilować, co nie ma prawa zadziałać przy próbie uruchomienia. Visual Studio zrozumiało to przede mną, a nawet podpowie nam w co najmniej dwóch miejscach jaka jest prawdopodobna przyczyna.

W dolnej części VS wyświetlane są błędy, którymi obarczony jest nasz program.

Chciałem wlać do baku naszej metody świeżo wydobytą ropę naftową co niechybnie skończyłoby się zniszczeniem silnika. Najpierw powinienem dokonać odpowiedniej rafinacji surowego stringa pobranego przez użytkownika i zamienić go na coś, co nasza metoda toleruje na wejściu, czyli na liczbę zmiennoprzecinkową – typ double.

Procedura zamiany stringów na typy liczbowe nazywa się parse’owaniem i posiada dwa bazowe warianty:

        string widthFromUser = Console.ReadLine();

        double width = double.Parse(widthFromUser);

        double saferWidth;
        bool converstionStatus = double.TryParse(widthFromUser, out saferWidth);

Wariant pierwszy, szybki to trzecia linijka z powyższego listingu. Przy jej pomocy wydajemy proste polecenie: zamień argument metody Parse na typ double.

Wariant drugi, czyli linijki piąta i szósta jest bardziej zachowawczy. Mówi on: spróbuj zamienić wartość tekstową na liczbę zmiennoprzecinkową.

Różnicę pomiędzy ‘zrób’ a ‘spróbuj’ odczujesz w sytuacji, gdy użytkownik sprawi Ci psikusa i nie poda liczby tylko losowy ciąg znaków, którego nie da się zamienić na liczbę. Wtedy zwykłe double.Parse wywoła wyjątek, czyli nieoczekiwany stan aplikacji, który w tym wariancie, zakończy nagle działanie programu. double.TryParse w kryzysowym momencie nie rzuci wyjątkiem, tylko elegancko zwróci wartość false jako dowód, że konwersja się nie powiodła, że coś poszło w tym miejscu nie tak. Zawsze zakładaj, że użytkownik skorzysta z Twojego narzędzia niezgodnie z zakładanym przeznaczeniem. Oczekuj nieoczekiwanego a Twoje życie będzie prostsze – Grzegorz Coehlo.

Obsługę wejścia użytkownika proponuję rozwiązać w poniższy sposób:

        string widthFromUser = Console.ReadLine();
        double width;
        if (!double.TryParse(widthFromUser, out width))
        {
            Console.WriteLine($"Wartość {widthFromUser} nie może zostać zamieniona na liczbę. Koniec działania programu.");
            return;
        }

Skorzystałem z bezpiecznego TryParse, jednocześnie informując użytkownika dlaczego program zakończył pracę. Jeżeli wszystko poszłoby ok, to zawartość instrukcji if nie zostanie wykonana – zwróc uwagę na wykrzyknik przed wywołaniem metody double.TryParse. W normalnych warunkach, gdy konwersja się powiedzie TryParse zwróci wartość true, my chcemy obsłużyć przypadek przeciwny i to właśnie wykrzyknik niejako zamienia true na false i odwrotnie.

Teraz uważaj, założ na głowę kask budowlańca, bo za chwilę zwali nam się na głowę cały programistyczny świat grzmiąc ‘nigdy, przenigdy nie programuj metodą Kopijego-Pejsta‘. Ja założyłem, dlatego przedstawiam Ci kolejny listing:

        string widthFromUser = Console.ReadLine();
        double width;
        if (!double.TryParse(widthFromUser, out width))
        {
            Console.WriteLine($"Wartość {widthFromUser} nie może zostać zamieniona na liczbę. Koniec działania programu.");
            return;
        }

        string heightFromUser = Console.ReadLine();
        double height;
        if (!double.TryParse(heightFromUser, out height))
        {
            Console.WriteLine($"Wartość {heightFromUser} nie może zostać zamieniona na liczbę. Koniec działania programu.");
            return;
        }

Widzisz uderzające podpobieństwo przy drugiej próbie konwersji tekstu na liczbę? Wykorzystaliśmy dokładnie ten sam ‘algorytm’ zmiany, zwracamy ten sam komunikat, złamaliśmy dobrą zasadę DRY, czyli don’t repeat yourself. Kopiowanie fragmentów kodu w różne miejsca niemal zawsze mści się po pewnym czasie, gdy okazuje się, że musimy coś zmienić i zapominamy w ilu miejscach znajduje się dana funkcjonalność. Jak zrobić to porządniej? Wyekstrahować funkcjonalność do nowej metody, na przykład tak:

    public static double? GetDoubleFromUser(string inputValue)
    {
        double result;
        if (double.TryParse(inputValue, out result))
            return result;
        else
        {
            Console.WriteLine($"Wartość {inputValue} nie może zostać zamieniona na liczbę. Koniec działania programu.");
            return null;
        }
    }

Zwróć uwagę na znak zapytania przy deklaracji zwracanego typu z naszej nowej metody. Zapis double? oznacza, że dana metoda może, ale nie musi zwrócić liczby. W przypadku niepowodzenia zwrócimy wartość null, którą traktuj jako nieistniejącą.

Po stworzeniu nowej metody GetDoubleFromUser możemy nasz program zapisać w poniższej postaci:

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Podaj szerokość przekroju:");
        string widthFromUser = Console.ReadLine();
        double? width = GetDoubleFromUser(widthFromUser);
        if (!width.HasValue)
            return;

        Console.WriteLine("Podaj wysokość przekroju:");
        string heightFromUser = Console.ReadLine();
        double? height = GetDoubleFromUser(heightFromUser);
        if (!height.HasValue)
            return;

        double momentOfInertia = GetMomentOfInertia(width.Value, height.Value);

        string message = string.Format("Moment bezwładności przekroju wynosi {0:N2}", momentOfInertia);
        Console.WriteLine(message);

        Console.ReadLine();
    }

    public static double? GetDoubleFromUser(string inputValue)
    {
        double result;
        if (double.TryParse(inputValue, out result))
            return result;
        else
        {
            Console.WriteLine($"Wartość {inputValue} nie może zostać zamieniona na liczbę. Koniec działania programu.");
            Console.ReadLine();
            return null;
        }
    }

    public static double GetMomentOfInertia(double b, double h)
    {
        double momentOfInertia = (b * Math.Pow(h, 3)) / 12;

        return momentOfInertia;
    }
}

Dodałem wskazówki/zachęty dla użytkownika przed właściwym odczytaniem wartości – zawsze to milej gdy program podpowie co teraz powinno się stać.

Nasza pierwsza aplikacja konsolowa działa, pobiera od użytkownika wejście, konwertuje wartości, wykonuje zadane przez nas działanie (wylicza moment bezwładności) i zwraca rezultat do konsoli. Nie jest źle, ale malkontenci powiedzą, że na kalkulatorze byłoby szybciej. W kolejnej części wzbogacimy naszą procedurę o kolejne elementy tak, żeby team Casio zaczął zmieniać zdanie 🙂