Krótko mówiąc, nie konstruuj swojego oprogramowania pod kątem możliwości ponownego użycia, ponieważ żaden użytkownik końcowy nie dba o to, czy twoje funkcje mogą być ponownie użyte. Zamiast tego inżynier zrozumiałości projektu - czy mój kod jest łatwy do zrozumienia dla kogoś innego lub dla mojej przyszłej zapomnianej osoby? - i elastyczność projektowania- kiedy nieuchronnie muszę naprawić błędy, dodać funkcje lub w inny sposób zmodyfikować funkcjonalność, w jakim stopniu mój kod będzie odporny na zmiany? Jedyne, na czym zależy Twojemu klientowi, to jak szybko możesz odpowiedzieć, kiedy zgłosi błąd lub poprosi o zmianę. Nawiasem mówiąc, zadawanie tych pytań na temat projektu powoduje, że kod nadaje się do ponownego użycia, ale takie podejście pozwala skupić się na unikaniu prawdziwych problemów, które napotkasz w ciągu całego życia tego kodu, dzięki czemu możesz lepiej służyć użytkownikowi końcowemu, niż zajmować się wzniosłym, niepraktycznym „inżynieryjne” ideały zadowolenia karku.
W przypadku czegoś tak prostego, jak podany przykład, początkowa implementacja jest dobra ze względu na to, jak niewielka jest, ale ten prosty projekt będzie trudny do zrozumienia i kruchy, jeśli spróbujesz wcisnąć zbyt dużą elastyczność funkcjonalną (w przeciwieństwie do elastyczności projektowania) w jedna procedura. Poniżej znajduje się wyjaśnienie mojego preferowanego podejścia do projektowania złożonych systemów pod kątem zrozumiałości i elastyczności, które mam nadzieję pokażą, co mam na myśli. Nie zastosowałbym tej strategii do czegoś, co można by napisać w mniej niż 20 wierszach w jednej procedurze, ponieważ coś tak małego już spełnia moje kryteria zrozumiałości i elastyczności.
Przedmioty, a nie Procedury
Zamiast używać klas takich jak oldskulowe moduły z szeregiem wywoływanych procedur w celu wykonywania czynności, które powinno robić oprogramowanie, rozważ modelowanie domeny jako obiektów, które oddziałują na siebie i współpracują w celu wykonania danego zadania. Metody w paradygmacie zorientowanym obiektowo zostały pierwotnie stworzone, aby były sygnałami między obiektami, dzięki czemu Object1mogły powiedzieć, Object2że robią coś, cokolwiek to jest, i być może otrzymać sygnał zwrotny. Wynika to z faktu, że paradygmat zorientowany obiektowo polega na modelowaniu obiektów domeny i ich interakcji, a nie na wymyślnym sposobie organizowania tych samych starych funkcji i procedur paradygmatu imperatywnego. W przypadkuvoid destroyBaghdadna przykład, zamiast próbować pisać bezkontekstową ogólną metodę radzenia sobie ze zniszczeniem Bagdadu lub jakiejkolwiek innej rzeczy (która może szybko stać się złożona, trudna do zrozumienia i krucha), każda rzecz, która może zostać zniszczona, powinna być odpowiedzialna za zrozumienie, w jaki sposób zniszczyć się. Na przykład masz interfejs opisujący zachowanie rzeczy, które można zniszczyć:
interface Destroyable {
void destroy();
}
Masz miasto, które implementuje ten interfejs:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nic, co wymaga zniszczenia instancji City, nigdy nie będzie miało znaczenia , jak to się stanie, więc nie ma powodu, aby ten kod istniał gdziekolwiek poza City::destroy, a rzeczywiście, intymna znajomość wewnętrznych mechanizmów działania Citysamego siebie byłaby ścisłym sprzężeniem, które zmniejsza elastyczność, ponieważ musisz wziąć pod uwagę te elementy zewnętrzne, jeśli kiedykolwiek będziesz musiał zmodyfikować zachowanie City. To jest prawdziwy cel enkapsulacji. Pomyśl o tym, jakby każdy obiekt miał swój własny interfejs API, który powinien umożliwić ci zrobienie z nim wszystkiego, co musisz, abyś mógł się martwić spełnieniem twoich żądań.
Delegacja, a nie „kontrola”
Teraz, czy twoja klasa wdrażająca jest, Cityczy Baghdadzależy od tego, jak ogólny jest proces niszczenia miasta. Najprawdopodobniej a Citybędzie składać się z mniejszych elementów, które będą musiały zostać zniszczone indywidualnie, aby doprowadzić do całkowitego zniszczenia miasta, więc w takim przypadku każdy z tych elementów również się wdroży Destroyable, i każdy z nich zostanie poinstruowany, Cityaby zniszczyć sami w taki sam sposób, jak ktoś z zewnątrz poprosił Cityo samozniszczenie.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
Jeśli chcesz naprawdę oszaleć i wdrożyć ideę Bombupuszczenia na lokalizację i niszczy wszystko w określonym promieniu, może to wyglądać mniej więcej tak:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadiusreprezentuje zestaw obiektów, który jest obliczany na podstawie Bombdanych wejściowych, ponieważ Bombnie ma znaczenia, w jaki sposób obliczenia są wykonywane, o ile może on działać z obiektami. Nawiasem mówiąc, jest to wielokrotnego użytku, ale głównym celem jest odizolowanie obliczeń od procesów upuszczania Bombi niszczenia obiektów, abyś mógł zrozumieć każdy element i sposób, w jaki pasują do siebie, i zmienić zachowanie pojedynczego elementu bez konieczności przekształcania całego algorytmu .
Interakcje, nie algorytmy
Zamiast próbować odgadnąć odpowiednią liczbę parametrów dla złożonego algorytmu, bardziej sensowne jest modelowanie procesu jako zestawu interaktywnych obiektów, z których każdy ma niezwykle wąskie role, ponieważ daje to możliwość modelowania złożoności twojego przetwarzaj przez interakcje między tymi dobrze zdefiniowanymi, łatwymi do zrozumienia i prawie niezmiennymi obiektami. Prawidłowe wykonanie sprawia, że nawet niektóre z najbardziej złożonych modyfikacji są tak trywialne, jak implementacja jednego lub dwóch interfejsów i przerobienie, które obiekty są tworzone w twojej main()metodzie.
Dałbym ci coś do twojego oryginalnego przykładu, ale szczerze mówiąc, nie mogę zrozumieć, co to znaczy „wydrukować ... Oszczędności na światło dzienne”. Co mogę powiedzieć o tej kategorii problemu, to to, że za każdym razem, gdy wykonujesz obliczenia, których wynik można sformatować na wiele sposobów, mój preferowany sposób na rozbicie tego wygląda następująco:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Ponieważ twój przykład używa klas z biblioteki Java, które nie obsługują tego projektu, możesz po prostu użyć interfejsu API ZonedDateTimebezpośrednio. Chodzi o to, że każde obliczenie jest zawarte w swoim własnym obiekcie. Nie przyjmuje żadnych założeń co do tego, ile razy powinien działać i jak sformatować wynik. Dotyczy wyłącznie wykonywania najprostszej formy obliczeń. To sprawia, że jest łatwy do zrozumienia i elastyczny w zmianie. Podobnie Resultdotyczy wyłącznie enkapsulacji wyniku obliczeń, a FormattedResultdotyczy wyłącznie interakcji z Resultformatowaniem zgodnie z określonymi przez nas regułami. W ten sposób,możemy znaleźć idealną liczbę argumentów dla każdej z naszych metod, ponieważ każda z nich ma dobrze zdefiniowane zadanie . O wiele łatwiej jest modyfikować poruszanie się do przodu, o ile interfejsy się nie zmieniają (co nie jest tak prawdopodobne, jeśli odpowiednio zminimalizujesz odpowiedzialność za swoje obiekty). Naszamain()metoda może wyglądać następująco:
class App {
public static void main(String[] args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
W rzeczywistości programowanie zorientowane obiektowo zostało wymyślone specjalnie jako rozwiązanie problemu złożoności / elastyczności paradygmatu imperatywnego, ponieważ nie ma po prostu dobrej odpowiedzi (na którą każdy może się zgodzić lub niezależnie), jak optymalnie określić funkcje imperatywne i procedury w ramach tego idiomu.