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 Object1
mogł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 destroyBaghdad
na 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 City
samego 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, City
czy Baghdad
zależy od tego, jak ogólny jest proces niszczenia miasta. Najprawdopodobniej a City
bę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, City
aby zniszczyć sami w taki sam sposób, jak ktoś z zewnątrz poprosił City
o 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ę Bomb
upuszczenia 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);
}
}
ObjectsByRadius
reprezentuje zestaw obiektów, który jest obliczany na podstawie Bomb
danych wejściowych, ponieważ Bomb
nie 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 Bomb
i 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 ZonedDateTime
bezpoś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 Result
dotyczy wyłącznie enkapsulacji wyniku obliczeń, a FormattedResult
dotyczy wyłącznie interakcji z Result
formatowaniem 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.