- Koniec podstaw – co dalej?
- Czy ufasz programom?
- Węzeł betoniarski
- Moment bezwładności przekroju niezarysowanego
- Wprowadźmy klasy
- Czarne jak konsola
- Visual Studio – instalacja
- C# – przedsionek
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.
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 🙂