Rozszerzalny kod

0

Siema

Każdy wie, że trzeba pisać kod, który jest rozszerzalny / extensible, w końcu to oczywiste... no i właśnie im dłużej o tym myślę, tym bardziej się utwierdzam w przekonaniu, że ja takiego pisać nie umiem.

Teorię powiedzmy że znam - kod powinien być otwarty na rozszerzenia, czyli żeby drugi programista mógł dodać jakąś rzecz bez zmieniania obecnego kodu. No i właśnie, co tak naprawdę, odwołując się do konkretnego kodu, to znaczy?? Przecież, koniec końców, gdzieś ta zmiana musi być - niżej lub wyżej w stosie wywołań metod, no ale chyba musi być...

Załóżmy, że mamy sklep internetowy, i można w nim oprócz dodawania produktu i jego kupna, również ocenić produkt, wystawić mu recenzję. I jak tu napisać jakiś serwis do wystawiania recenzji danego produktu, żeby był rozszerzalny?

Jakie konkretne techniki w javie możemy zastosować? Interfejsy? Ale koniec końców i tak się gdzieś podaje konkretną implementację, więc w tamtym miejscu i tak trzeba będzie zmienić. Dziedziczenie i korzystanie z klasy bazowej? Nadal chyba gdzieś ta zmiana musiałaby być poczyniona.

A jak już dołożymy do tego enterprise standardy, jak np JPA (no jest słabe, ale jestem do niego najbardziej przyzwyczajony) i ostatecznie wychodzi mi taka aplikacja, gdzie załóżmy podział pakietowy mam nawet spoko, ale nadal w każdym pakiecie mam jakiś kontroler, jakiś dto, jakiś mapper... czy to jest extensible code?

7
Pinek napisał(a):

Siema

Każdy wie, że trzeba pisać kod, który jest rozszerzalny / extensible, w końcu to oczywiste...

Otóż nie. Nie każdy wie. Ja nie wiem. I nie uważam, że trzeba pisać kod rozszerzalny.
Trzeba pisać kod przystosowany do zmian (utrzymywalny).

Wydaje mi się, że 15 lat temu to też (rozszerzalność FTW) było dla mnie oczywiste. Teraz nie wiem nawet czemu.

5

Tu nie chodzi o kompletny brak zmian, tylko o wymianę implementacji w kilku łatwo kontrolowanych miejscach, na przykład w kontenerze DI lub konfiguracji. Przydają się interfejsy, strategie, fabryki itp.

Co do oczywistości pisania takiego kodu, to moim zdaniem lepiej pisać kod, który da się łatwo i szybko przepisać bez orania całej aplikacji, czyli jakieś sensowne komponenty itp. Najlepszy jest kod, który można łatwo wyrzucić i zastąpić nowym, to bardzo upraszcza ciągle dyskusje o nazwach zmiennych i jakości testów.

1

Pytanie podstawowe brzmi: rozszerzalny w jakim kierunku? Polecam lekturę https://www.thoughtworks.com/books/building-evolutionary-architectures

W teorii, aby przygotować kod na rozszerzenia, trzeba znać plan jego rozwoju. Przykład. Dawno temu projektowałem model danych pod sameday delivery, przy czym współpracowaliśmy tylko z jednym przewoźnikiem. Podjęliśmy świadomą decyzję, że robimy model zamknięty na rozszerzenia, ponieważ tak będzie szybciej plus nie znamy domeny i nie wiemy w jakim kierunku miałby się rozszerzać - okienka doręczenia, godziny podjazdów etc. W końcu przez jeden punkt przebiega nieskończenie wiele prostych, a mnie uczono, żeby zawsze takich punktów mieć co najmniej 3 :)

6

Dopóki nie piszemy jakiegoś API, od którego zależny jest kod klientów, to raczej każda próba to masturbowania się z Open Closed Principle NA SIŁĘ łamie YAGNI i zazwyczaj potem kończy się tym, że tworzone są interfejsy z jedną konkretną implementacją, a unit testy to testy mocków :)

5

Zapomniałeś o tym co jest wczesniej, czyli separacja.

Musisz miec wczesniej odseparowane kawałki kodu, o to warto się bić, zwłaszcza, że taka rzecz stosunkowo łatwo przychodzi jak stosujesz się do DRY. Separacja to dobra rzecz niezależnie od języka i pomaga zepchnąć szczegóły na dalszy plan. Praca wymaga na ogół skupienia, a skupienie najłatwiej idzie utrzymać gdy w kodzie zajmujesz się jedną rzeczą na raz.

Niestety nie na długo się nacieszysz, po pewnym czasie okazuje się, że masz pewną pulę podobnych operacji, albo że musisz te operacje stosować zamiennie i ogólnie siłą rzeczy dochodzi do pewnych powtórzeń, czasem do tego etapu dochodzisz szybciej (barokowy język) czasem wolniej (język z makrami).

No i tutaj możesz możesz próbować sprowadzać temat do wspólnego interfejsu, klasy bazowej - co wymaga doświadczenia, bo najlepsze interfejsy / klasy bazowe pozwalają na mało operacji. Dzięki temu bronią się dłużej przed złamaniem kompatybilności. W każdym razie ten etap to jest moment, gdy intensywnie korzystasz z OOP lub FP.

I już na tym etapie uzyskujesz rozszerzalny kod. Co zyskujesz jak masz rozszerzalny kod? W zasadzie to mnie cieszy fakt, że jak pomysł z implementacja nie wypali to usunięcie kodu nie będzie wymagalo wiele prac. Po prostu odpinam implementacje, i na jej miesce wstawiam coś innego. Taki łatwiej można dostosować do zmieniających się okoliczności.

Jak zrobisz krok dalej, czyli odseparujesz od siebie rzeczy, które da się ze sobą składać (compose) wtedy wychodzą Ci wzorce.

WSKAZÓWKI:

  1. Nie twórz złożonych klas / intefejsów - takie ściągają zło na kod i one obrywają pierwsze, gdy klient zmieni wymagania.

  2. Opóźniaj tworzenie rozszerzalnego kodu, ponieważ brak świadomości o dziedzinie skutkować będzie niekompletnym interfejsem. Poza tym jak masz 2-3 podobne przypadki to jeszcze nie jest powód, aby wydzielać wspólny interfejs. Pozwól im leżakować, poczekaj aż sytuacja bardziej się skomplikuje i wtedy w ramach refaktoryzacji można odpowiedzieć w tym kierunku który najbardziej boli - wtedy będziesz mieć więcej wiedzy.

  3. Jak rozszerzalność przesuniesz na dalszy plan to szybciej bedziesz kodował, może mniej perfekcyjnie, ale za to skutecznie.

EDIT:

PS, warto zwrócić uwagę, że rozszerzalność ogólnie ładnie prezentuje się na przykładzie bibliotek / frameworków. Dzieje się tak, że tam wymagania rzadko się drastycznie się zmieniają. Jeśli biblioteka nie pozwala na jakąś operacje no to cóż.. szuka się innej biblioteki.

Natomiast w przypadku biznesu i produktów zmana wymagań może być nawet o 180' ponieważ wraz z tą zmianą chodzi o hajs, z resztą o to chodzi od samego początku więc taka rzecz nie powinna dziwić.

2
ret napisał(a):

PS, warto zwrócić uwagę, że rozszerzalność ogólnie ładnie prezentuje się na przykładzie bibliotek / frameworków. Dzieje się tak, że tam wymagania rzadko się drastycznie się zmieniają. Jeśli biblioteka nie pozwala na jakąś operacje no to cóż.. szuka się innej biblioteki.

No właśnie nie. To skopane biblioteki, których twórcy nie rozumieją OCP są takie, że trzeba szukać innych. Otwarty na rozszerzenia to nic innego jak możliwość dopisania brakującego kawałku kodu, który robi to czego oczekuję bez potrzeby modyfikowania biblioteki (zamknięty na modyfikacje).

4

Dopóki nie piszemy jakiegoś API, od którego zależny jest kod klientów, to raczej każda próba to masturbowania się z Open Closed Principle NA SIŁĘ
W teorii, aby przygotować kod na rozszerzenia, trzeba znać plan jego rozwoju.

Dokładnie !
Założmy na przykład że jest aplikacja bankowa w której robimy eksport histori do plików.
Teraz drogi są dwie:
1)Zakładamy że będzie to tylko PDF i tyle. Tworzymy kawałek kodu który nie polega na abstrakcjach i jest jakaś klasa/zestaw funkcji eksportujących do PDF, koniec.
2)Zakładamy że teraz to PDF i CSV, ale później ewentualnie może coś dojść. Robimy wtedy interfejs i np. enum jak ReportFileType. Można wtedy przekazac mapę Map<ReportFileType, ReportGenerator> albo zrobić np. ReportGenerator z metodą getSupportedFileType i wstrzyknąć kolekcje implementacji przez konstruktor. Warto pamiętać że kontenery IoC w żaden nsposób nie sa wymagane do tego :)

5

Z książki adaptywny kod, tak w ogóle spoko książka; mimo, ze prawi o SOLID to znaleźć w niej można uwagi odnoszące się racji technik i ryzyka z ich użyciem, oto jeden z paru ciekawych przypadków jaki został tam omówiony:

Początkujący programiści mają tendencję do tworzenia proceduralnego kodu, nawet w językach obiektowych takich jak C#. Wykorystują klasy w charakterze magazynów metod niezależnie od tgo czy metody te są ze sobą powiązane, czy nie.

(...)

Całkowitym przeciwieństwem jest programista, który od samego początku tworzy interfejsy. Odkrywa, że ma w ręce młotek, i wszystko kojarzy mu się z gwoździem. Utworzony przez niego kod jest naszpikowany punktami rozserzeń, z których więszkosć nigdy nie będzie wykorzystana. Napisanie takiego kodu, delegującego wszystkie operacje do interfejsów wymaga sporego nakładu, podobnie jak zrozumienie działania tego kodu.

(...)

Gdyby z tych dwóch skrajnie różniących się programistów zrobić jednego, wtedy utworzony przez niego kod byłby harmonijnym kompromisem zawierajacym odpowiednią liczbę punktów rozszerzeń i przystosowanym do wprowadzania zmian jedynie w funkcjonalnościach, które są niejasne, zmienne i trudne do zaimplementowania. Do tego jednak potrzebne jest doświadczenie

Kierunek: niejasne, zmienne i trudne do zaimplementowania i nagle z 50 przypadków pozostaje 3-5, które są elementem strategii w rozwoju projektu.

A jak ktoś opakowuje banały w setki klas, to ja się nie dziwie, że potem jest przerost formy nad trescią.

3

Ja rozszerzalny kod rozumiem tak:

public interface ItemReader {
    List<Tuple> read();
}
public interface Strategy {
    Integer apply(List<Tuple> tupleList);
}
public interface FacadeService {
    void result();
}
public class FacadeServiceImpl implements FacadeService {
    private final ItemReader itemReader;
    private final Strategy strategy;

    public FacadeServiceImpl(ItemReader itemReader, Strategy strategy) {
        this.itemReader = itemReader;
        this.strategy = strategy;
    }

    @Override
    public void result() {
        Integer result = strategy.apply(itemReader.read());
    }
}
public class Main {

    public static void main(String[] args) {
        new FacadeServiceImpl(
                new FlatFileItemReader(args[0]),
                new DisjointCompartmentsStrategy()
        ).result();
    }

}

Możesz stworzyć dowolną implementację spełniającą kontrakt założony przez twórcę interfejsów no i potem ją wykorzystać.

4

trzeba pisać kod, który jest

Nie trzeba. Trzeba to wyczuć moment, kiedy pisać coś w sposób rozszerzalny czy elastyczny, a kiedy jednak zrobić nieodporny na zmiany monolit (przy czym w jednym projekcie możesz mieć i taki i taki kod).

A przesada w każdą stronę jest zła. Bo raz napiszesz kompletnie nieelastyczny kod i będziesz cierpiał, bo zmienią się wymagania. Innym razem będziesz chciał być mądrzejszy i napiszesz kod tak super elastyczny, że potem sam się pogubisz w abstrakcjach, plus będziesz go robił 5 razy dłużej. Wtedy będziesz widział, że przesadziłeś.

KISS i YAGNI są trudne, bo wymagają oduczenia się pewnych naleciałości. Człowiek ileś lat się uczy, że mają być dobre praktyki i kod elastyczny itp. itd. a potem się okazuje, że KISS i YAGNI. Trzeba wejść na metapoziom i myśleć biznesowo czy strategicznie (co się lepiej opłaci). Też trzeba się cofnąć w rozwoju trochę i zamiast barokowych abstrakcji napisać po prostu skromnie kod, który coś robi.

Ale tak jak było wspomniane już - prostota API, izolacja modułów, a potem refaktoring w razie potrzeby.

1

Z rozszerzalnością to jak z backupem, niby każdy wie, ale nie każdy robi ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1