Radzenie sobie z przecięciami funkcji

11

Ostatnio widziałem coraz więcej problemów podobnych do tych wyjaśnionych w tym artykule na temat skrzyżowań funkcji. Innym terminem na to byłyby linie produktów, chociaż zwykle przypisuję je do faktycznie różnych produktów, podczas gdy zwykle napotykam te problemy w postaci możliwych konfiguracji produktów.

Podstawowa koncepcja tego rodzaju problemu jest prosta: dodajesz funkcję do produktu, ale jakoś się komplikuje z powodu kombinacji innych istniejących funkcji. W końcu QA znajduje problem z rzadką kombinacją funkcji, o których nikt wcześniej nie pomyślał, i to, co powinno być prostą poprawką, może nawet wymagać dużych zmian projektowych.

Wymiary tego problemu przecięcia cech są zadziwiającą złożonością. Załóżmy, że obecna wersja oprogramowania ma Nfunkcje i dodajesz jedną nową funkcję. Uprośćmy też, mówiąc, że każdą z funkcji można włączyć lub wyłączyć tylko, wtedy masz już 2^(N+1)możliwe kombinacje funkcji do rozważenia. Ze względu na brak lepszego sformułowania / wyszukiwanych terminów, istnienie tych kombinacji nazywam problemem przecięcia cech . (Punkty bonusowe za odpowiedź, w tym referencje za bardziej ustalony termin).

Teraz mam problem z tym, jak poradzić sobie z tym problemem złożoności na każdym poziomie procesu programowania. Z oczywistych powodów związanych z kosztami nie jest praktyczne do tego stopnia, że ​​jest utopijny, aby osobno zająć się każdą kombinacją. W końcu staramy się trzymać z dala od algorytmów wykładniczej złożoności nie bez powodu, ale przekształcenie samego procesu rozwoju w potwora wielkości wykładniczej z pewnością doprowadzi do całkowitej awarii.

Jak więc uzyskać najlepszy wynik w systematyczny sposób, który nie rozbija żadnych budżetów i jest kompletny w przyzwoity, użyteczny i profesjonalnie akceptowalny sposób.

  • Specyfikacja: Kiedy określisz nową funkcję - w jaki sposób zapewnisz, że będzie się dobrze bawić ze wszystkimi innymi dziećmi?

    Widzę, że można systematycznie badać każdą istniejącą funkcję w połączeniu z nową funkcją - ale byłoby to w oderwaniu od innych funkcji. Biorąc pod uwagę złożoną naturę niektórych funkcji, ten odizolowany widok jest już często tak zaangażowany, że potrzebuje ustrukturyzowanego podejścia sam w sobie, nie mówiąc już o 2^(N-1)czynniku spowodowanym przez inne cechy, które jeden z nich zignorował.

  • Implementacja: Kiedy implementujesz funkcję - w jaki sposób upewniasz się, że Twój kod działa prawidłowo / przecina się we wszystkich przypadkach.

    Znów zastanawiam się nad samą złożonością. Znam różne techniki zmniejszania potencjału błędu dwóch przecinających się cech, ale żadna z nich nie skalowałaby się w żaden rozsądny sposób. Zakładam jednak, że dobra strategia podczas specyfikacji powinna trzymać problem na dystans podczas wdrażania.

  • Weryfikacja: Kiedy testujesz funkcję - jak sobie radzisz z faktem, że możesz przetestować tylko ułamek tego miejsca przecięcia funkcji?

    Trudno jest wiedzieć, że testowanie pojedynczej funkcji w izolacji nie gwarantuje niczego w pobliżu kodu bezbłędnego, ale kiedy zredukujesz to do ułamka 2^-N, wydaje się, że setki testów nie obejmują nawet jednej kropli wody we wszystkich oceanach łącznie . Co gorsza, najbardziej problematyczne są te, które wynikają ze przecięcia się cech, których nie można oczekiwać, że doprowadzą do jakichkolwiek problemów - ale jak je przetestować, jeśli nie spodziewasz się tak silnego przecięcia?

Chociaż chciałbym usłyszeć, jak inni radzą sobie z tym problemem, interesuje mnie przede wszystkim literatura lub artykuły analizujące temat bardziej szczegółowo. Więc jeśli osobiście przestrzegasz określonej strategii, dobrze byłoby dołączyć odpowiednie odpowiedzi do swojej odpowiedzi.

Szczery
źródło
6
Rozsądnie zaprojektowana architektura aplikacji może pomieścić nowe funkcje bez przewracania świata do góry nogami; doświadczenie jest tutaj świetnym niwelatorem. To powiedziawszy, taka architektura nie zawsze jest łatwa do zrobienia za pierwszym razem, a czasem trzeba dokonać trudnych zmian. Problem z testowaniem niekoniecznie musi być problemem, jeśli wiesz, jak odpowiednio obudować funkcje i funkcje i objąć je odpowiednimi testami jednostkowymi.
Robert Harvey

Odpowiedzi:

6

Wiedzieliśmy już matematycznie, że weryfikacja programu jest niemożliwa w skończonym czasie w najbardziej ogólnym przypadku z powodu problemu zatrzymania. Ten rodzaj problemu nie jest nowy.

W praktyce dobry projekt może zapewnić odsprzęganie w taki sposób, że liczba przecinających się elementów jest znacznie mniejsza niż 2 ^ N, choć z pewnością wydaje się, że jest powyżej N, nawet w dobrze zaprojektowanych systemach.

Jeśli chodzi o źródła, wydaje mi się, że prawie każda książka lub blog o projektowaniu oprogramowania skutecznie stara się zmniejszyć tę 2 ^ N tak bardzo, jak to możliwe, chociaż nie znam żadnej, która rzucałaby ten problem na te same warunki, co ty zrobić.

Na przykład, w jaki sposób projekt może w tym pomóc, we wspomnianym artykule nastąpiło przecięcie niektórych funkcji, ponieważ replikacja i indeksowanie zostały wywołane eTag. Gdyby mieli dostępny inny kanał komunikacyjny do sygnalizowania potrzeby każdego z nich osobno, być może mogliby łatwiej kontrolować kolejność zdarzeń i mieć mniej problemów.

Albo może nie. Nic nie wiem o RavenDB. Architektura nie może zapobiec problemom z przecięciem funkcji, jeśli funkcje są naprawdę niewytłumaczalnie splecione i nigdy nie możemy wiedzieć z góry, że nie będziemy chcieli funkcji, która naprawdę ma najgorszy przypadek przecięcia 2 ^ N. Ale architektura może przynajmniej ograniczyć skrzyżowania z powodu problemów z implementacją.

Nawet jeśli mylę się co do RavenDB i eTagów (i używam go tylko ze względu na kłótnie - to mądrzy ludzie i prawdopodobnie dobrze to zrozumieli), powinno być jasne, w jaki sposób architektura może pomóc. Większość wzorców, o których ludzie mówią, zostały zaprojektowane bezpośrednio w celu zmniejszenia liczby zmian kodu wymaganych przez nowe lub zmieniające się funkcje. Cofnie się to wstecz - na przykład „Wzorce projektowe, elementy oprogramowania obiektowego wielokrotnego użytku”, wprowadzenie stwierdza: „Każdy wzorzec projektowy pozwala na zmianę niektórych aspektów architektury niezależnie od innych aspektów, dzięki czemu system jest bardziej odporny na określony rodzaj zmiana".

Chodzi mi o to, że można zrozumieć w praktyce Wielką O przecięć cech w praktyce, patrząc na to, co dzieje się w praktyce. Badając tę ​​odpowiedź, stwierdziłem, że większość analiz punktów funkcyjnych / wysiłku rozwojowego (tj. - produktywności) wykazała albo mniej niż liniowy wzrost wysiłku projektu na punkt funkcji, albo bardzo nieznacznie powyżej wzrostu liniowego. Co mnie trochę zaskoczyło. To miał dość czytelny przykład.

To (i podobne badania, w których niektóre wykorzystują punkty funkcyjne zamiast wierszy kodu) nie dowodzą, że przecięcie cech nie występuje i nie powoduje problemów, ale wydaje się rozsądnym dowodem na to, że w praktyce nie jest to katastrofalne.

psr
źródło
0

W żadnym wypadku nie będzie to najlepsza odpowiedź, ale zastanawiałem się nad niektórymi rzeczami, które przecinają się z punktami twojego pytania, więc pomyślałem, że o nich wspomnę:

Wsparcie strukturalne

Z tego, co widziałem, gdy funkcje są błędne i / lub nie współgrają z innymi, jest to w dużej mierze spowodowane słabym wsparciem zapewnianym przez podstawową strukturę / strukturę programu do zarządzania / koordynowania nimi. Wydaje mi się, że poświęcenie większej ilości czasu na dopracowanie i zaokrąglenie rdzenia powinno ułatwić dodanie nowych funkcji.

Jedną z rzeczy, które uważam za powszechne w aplikacjach, w których pracuję, jest to, że struktura programu została skonfigurowana do obsługi jednego rodzaju obiektu lub procesu, ale wiele rozszerzeń, które zrobiliśmy lub chcemy mieć zrobić z obsługą wielu rodzajów. Gdyby wziąć to pod uwagę bardziej na początku projektowania aplikacji, pomogłoby to dodać te funkcje później.

Staje się to dość krytyczne, gdy dodaje się obsługę wielu X-ów, które obejmują kod sterowany wątkami / asynchronicznie / zdarzeniami, ponieważ te rzeczy mogą się dość szybko popsuć - miałem przyjemność debugować wiele problemów z tym związanych.

Prawdopodobnie trudno jest usprawiedliwić ten rodzaj wysiłku z góry, szczególnie w przypadku prototypów lub projektów jednorazowych - nawet jeśli niektóre z tych prototypów lub projektów jednorazowych będą ponownie używane lub jako (podstawa) ostatecznego systemu, co oznacza, że ​​wydatki byłyby tego warte na dłuższą metę.

Projekt

Projektując rdzeń programu, korzystanie z podejścia odgórnego może pomóc zmienić rzeczy w porcje do zarządzania i pozwala ominąć problematyczną domenę; potem myślę, że powinno się zastosować podejście oddolne - pomoże to uczynić rzeczy mniejszymi, bardziej elastycznymi i lepszymi do późniejszego dodania. (Jak wspomniano w linku, robienie tego w ten sposób zapewnia mniejsze implementacje funkcji, co oznacza mniej konfliktów / błędów).

Jeśli skupisz się na podstawowych elementach systemu i upewnisz się, że wszystkie one dobrze oddziałują, to wszystko zbudowane przy ich użyciu prawdopodobnie również będzie się dobrze zachowywać i powinno lepiej integrować się z resztą systemu.

Kiedy dodawana jest nowa funkcja, myślę, że przy projektowaniu jej można podążać podobną drogą, jak przy projektowaniu reszty frameworka: rozkładanie go, a następnie przechodzenie od dołu do góry. Jeśli możesz ponownie użyć któregokolwiek z oryginalnych bloków z frameworka przy implementacji tej funkcji, byłoby to zdecydowanie pomocne; gdy skończysz, możesz dodawać nowe bloki, które otrzymujesz z tej funkcji do tych, które są już w podstawowej strukturze, testując je z oryginalnym zestawem bloków - w ten sposób będą one kompatybilne z resztą systemu i będą mogły być używane w przyszłości funkcje również.

Uproszczać!

Ostatnio zajmowałem się minimalistycznym podejściem do projektowania, zaczynając od uproszczenia problemu, a następnie uproszczenia rozwiązania. Jeśli można poświęcić sekundę, upraszczając iterację projektu, to widzę, że jest to bardzo pomocne przy późniejszym dodawaniu rzeczy.

W każdym razie to moje 2c.

Paweł
źródło