Pracuję nad dużym projektem oprogramowania, który jest wysoce dostosowany do potrzeb różnych klientów na całym świecie. Oznacza to, że mamy może 80% kodu, który jest wspólny dla różnych klientów, ale także dużo kodu, który musi się zmieniać z jednego klienta na drugiego. W przeszłości zajmowaliśmy się tworzeniem oddzielnych repozytoriów (SVN), a kiedy rozpoczął się nowy projekt (mamy niewielu, ale dużych klientów), utworzyliśmy kolejne repozytorium w oparciu o wcześniejszy projekt, który ma najlepszą bazę kodu dla naszych potrzeb. To działało w przeszłości, ale mieliśmy kilka problemów:
- Błędy naprawione w jednym repozytorium nie są załatane w innych repozytoriach. Może to być problem z organizacją, ale trudno mi naprawić i załatać błąd w 5 różnych repozytoriach, pamiętając, że zespół utrzymujący to repozytorium może znajdować się w innej części świata i nie mamy ich środowiska testowego , nie znają harmonogramu ani wymagań, jakie mają („błąd” w jednym kraju może być „cechą” w innym).
- Funkcje i ulepszenia wprowadzone dla jednego projektu, które mogą być również przydatne w innym projekcie, są tracone lub jeśli są używane w innym projekcie, często powodują duże problemy z łączeniem ich z jednej bazy kodu do drugiej (ponieważ obie gałęzie mogły być rozwijane niezależnie przez rok ).
- Refaktoryzacje i ulepszenia kodu dokonane w jednej gałęzi programistycznej zostają utracone lub powodują więcej szkody niż pożytku, jeśli trzeba scalić wszystkie te zmiany między gałęziami.
Dyskutujemy teraz, jak rozwiązać te problemy i do tej pory wymyśliliśmy następujące pomysły, jak to rozwiązać:
Utrzymuj rozwój w oddzielnych gałęziach, ale lepiej organizuj się, mając centralne repozytorium, w którym ogólne poprawki błędów są łączone, a wszystkie projekty scalają zmiany z tego centralnego repozytorium do swoich własnych regularnie (np. Codziennie). Wymaga to dużej dyscypliny i dużego wysiłku w celu połączenia oddziałów. Nie jestem więc przekonany, że to zadziała i możemy zachować tę dyscyplinę, zwłaszcza gdy pojawia się presja czasu.
Porzuć oddzielne gałęzie programistyczne i posiadaj centralne repozytorium kodu, w którym żyje cały nasz kod, i dostosowuj go, mając moduły wtykowe i opcje konfiguracji. Używamy już kontenerów wstrzykiwania zależności do rozwiązywania zależności w naszym kodzie i postępujemy zgodnie ze wzorcem MVVM w większości naszego kodu, aby w czysty sposób oddzielić logikę biznesową od naszego interfejsu użytkownika.
Drugie podejście wydaje się być bardziej eleganckie, ale mamy wiele nierozwiązanych problemów w tym podejściu. Na przykład: jak obsługiwać zmiany / uzupełnienia w modelu / bazie danych. Używamy .NET z Entity Framework, aby mieć silnie typowane podmioty. Nie rozumiem, w jaki sposób możemy obsłużyć właściwości wymagane dla jednego klienta, ale bezużyteczne dla innego klienta bez zaśmiecania naszego modelu danych. Myślimy o rozwiązaniu tego w bazie danych za pomocą tabel satelitarnych (posiadających osobne tabele, w których nasze dodatkowe kolumny dla konkretnego obiektu żyją z odwzorowaniem 1: 1 na oryginalny obiekt), ale jest to tylko baza danych. Jak sobie z tym poradzić w kodzie? Nasz model danych znajduje się w centralnej bibliotece, której nie bylibyśmy w stanie rozszerzyć dla każdego klienta stosującego to podejście.
Jestem pewien, że nie jesteśmy jedynym zespołem zmagającym się z tym problemem i jestem zszokowany, gdy znalazłem tak mało materiału na ten temat.
Więc moje pytania są następujące:
- Jakie masz doświadczenie z wysoce spersonalizowanym oprogramowaniem, jakie podejście wybrałeś i jak to zadziałało?
- Jakie podejście polecasz i dlaczego? Czy istnieje lepsze podejście?
- Czy są jakieś dobre książki lub artykuły na ten temat, które możesz polecić?
- Czy masz konkretne zalecenia dla naszego środowiska technicznego (.NET, Entity Framework, WPF, DI)?
Edytować:
Dziękuję za wszystkie sugestie. Większość pomysłów pasuje do tych, które już mieliśmy w naszym zespole, ale naprawdę pomocne jest zapoznanie się z ich doświadczeniem i wskazówkami, jak je lepiej wdrożyć.
Nadal nie jestem pewien, w którą stronę pójdziemy i nie podejmuję decyzji (sam), ale przekażę to zespołowi i jestem pewien, że będzie to pomocne.
W tej chwili tenor wydaje się być pojedynczym repozytorium wykorzystującym różne moduły specyficzne dla klienta. Nie jestem pewien, czy nasza architektura jest gotowa na to, ani ile musimy zainwestować, aby ją dopasować, więc niektóre rzeczy mogą przez jakiś czas żyć w osobnych repozytoriach, ale myślę, że to jedyne długoterminowe rozwiązanie, które zadziała.
Dziękuję raz jeszcze za wszystkie odpowiedzi!
Odpowiedzi:
Wygląda na to, że podstawowym problemem jest nie tylko utrzymanie repozytorium kodu, ale brak odpowiedniej architektury .
Frameworki lub standardowa biblioteka obejmuje te pierwsze, podczas gdy te drugie zostałyby zaimplementowane jako dodatki (wtyczki, podklasy, DI, cokolwiek ma sens dla struktury kodu).
Pomógłby także system kontroli źródła, który zarządza oddziałami i rozproszonym rozwojem; Jestem fanem Mercurial, inni wolą Git. Ramy byłyby główną gałęzią, każdy dostosowany system byłby na przykład gałęziami podrzędnymi.
Konkretne technologie zastosowane do wdrożenia systemu (.NET, WPF, cokolwiek) są w dużej mierze nieistotne.
Właściwe rozwiązanie tego problemu nie jest łatwe , ale ma kluczowe znaczenie dla długoterminowej rentowności. Oczywiście im dłużej będziesz czekać, tym większy będzie dług techniczny, z którym będziesz musiał sobie poradzić.
Przyda ci się książka Architektura oprogramowania: zasady i wzorce organizacyjne .
Powodzenia!
źródło
Jedna firma, w której pracowałem, miała ten sam problem, a podejście do rozwiązania tego problemu było następujące: Stworzono wspólne ramy dla wszystkich nowych projektów; obejmuje to wszystkie rzeczy, które muszą być takie same w każdym projekcie. Np. Narzędzia do generowania formularzy, eksport do Excela, logowanie. Podjęto wysiłki, aby upewnić się, że ta wspólna struktura jest ulepszona tylko (gdy nowy projekt wymaga nowych funkcji), ale nigdy się nie rozwidla.
W oparciu o te ramy kod specyficzny dla klienta był utrzymywany w osobnych repozytoriach. Gdy jest to przydatne lub konieczne, poprawki błędów i ulepszenia są kopiowane między projektami (ze wszystkimi zastrzeżeniami opisanymi w pytaniu). Jednak przydatne na całym świecie ulepszenia wchodzą we wspólne ramy.
Posiadanie wszystkiego we wspólnej bazie kodu dla wszystkich klientów ma pewne zalety, ale z drugiej strony, czytanie kodu staje się trudne, gdy istnieje niezliczona liczba
if
s, aby program zachowywał się inaczej dla każdego klienta.EDYCJA: Jedna anegdota, aby uczynić to bardziej zrozumiałym:
źródło
Jeden z projektów, nad którymi pracowałem, obsługiwał wiele platform (więcej niż 5) w wielu wydaniach produktów. Wiele wyzwań, które opisujesz, to rzeczy, z którymi się borykaliśmy, choć w nieco inny sposób. Mieliśmy zastrzeżoną bazę danych, więc nie mieliśmy takich samych problemów na tym polu.
Nasza struktura była podobna do twojej, ale mieliśmy jedno repozytorium dla naszego kodu. Kod specyficzny dla platformy wszedł do własnych folderów projektu w drzewie kodu. Wspólny kod mieszkał w drzewie na podstawie warstwy, do której należał.
Mieliśmy kompilację warunkową, opartą na budowanej platformie. Utrzymanie tego było dość uciążliwe, ale trzeba było to zrobić tylko wtedy, gdy dodano nowe moduły w warstwie specyficznej dla platformy.
Posiadanie całego kodu w jednym repozytorium ułatwiło nam usuwanie błędów na wielu platformach i wydaniach jednocześnie. Mieliśmy zautomatyzowane środowisko kompilacji dla wszystkich platform, które mogą służyć jako zabezpieczenie na wypadek, gdyby nowy kod złamał przypuszczalnie niepowiązaną platformę.
Próbowaliśmy go zniechęcić, ale zdarzałyby się przypadki, gdy platforma potrzebowała naprawy opartej na specyficznym dla platformy błędzie, który byłby w innym typowym kodzie. Jeśli moglibyśmy warunkowo zastąpić kompilację, nie powodując, że moduł wyglądałby na niezręcznie, zrobilibyśmy to najpierw. Jeśli nie, przenieślibyśmy moduł ze wspólnego terytorium i przenieśliśmy go na platformę.
W przypadku bazy danych mieliśmy kilka tabel, które miały kolumny / modyfikacje specyficzne dla platformy. Upewnilibyśmy się, że każda wersja platformy tabeli spełniała podstawowy poziom funkcjonalności, więc wspólny kod mógłby się do niej odwoływać bez obawy o zależności platformy. Zapytania / manipulacje specyficzne dla platformy zostały wepchnięte do warstw projektu platformy.
Aby odpowiedzieć na twoje pytania:
źródło
Przez wiele lat pracowałem nad aplikacją do administracji emerytalnej, która miała podobne problemy. Plany emerytalne różnią się znacznie między firmami i wymagają wysoce specjalistycznej wiedzy do wdrożenia logiki obliczeniowej i raportów, a także bardzo różnych projektów danych. Mogę tylko krótko opisać część architektury, ale może da to dość pomysłu.
Mieliśmy 2 oddzielne zespoły: główny zespół programistów , który był odpowiedzialny za kod systemu podstawowego (który byłby twoim 80% kodem współdzielonym powyżej) oraz zespół wdrożeniowy , który posiadał specjalistyczną wiedzę w zakresie systemów emerytalnych i był odpowiedzialny za uczenie się klienta wymagania i skrypty i raporty dla klienta.
Mieliśmy wszystkie nasze tabele zdefiniowane przez Xml (to przed czasem, gdy ramy encji były testowane pod względem czasu i wspólne). Zespół wdrażający zaprojektowałby wszystkie tabele w Xml, a podstawowa aplikacja mogłaby zostać poproszona o wygenerowanie wszystkich tabel w Xml. Dla każdego klienta były także powiązane pliki skryptów VB, Crystal Reports, Word Word itp. (W Xml był również wbudowany model dziedziczenia, aby umożliwić ponowne użycie innych implementacji).
Aplikacja podstawowa (jedna aplikacja dla wszystkich klientów) buforowałaby wszystkie rzeczy specyficzne dla klienta, gdy nadejdzie żądanie dla tego klienta, i wygenerowała wspólny obiekt danych (coś w rodzaju zdalnego zestawu rekordów ADO), który można serializować i przekazywać na około.
Ten model danych jest mniej elastyczny niż obiekty encji / domeny, ale jest bardzo elastyczny, uniwersalny i może być przetwarzany przez jeden zestaw podstawowego kodu. Być może w twoim przypadku możesz zdefiniować obiekty encji bazowej przy użyciu tylko wspólnych pól i mieć dodatkowy Słownik dla pól niestandardowych (dodaj do swojego obiektu encji pewien zestaw deskryptorów danych, aby zawierał metadane dla pól niestandardowych. )
Mieliśmy osobne repozytoria źródłowe dla kodu systemu podstawowego i kodu implementacji.
Nasz system podstawowy miał bardzo małą logikę biznesową, poza kilkoma bardzo standardowymi wspólnymi modułami obliczeniowymi. Podstawowy system działał jako: generator ekranu, skrypt uruchamiający, generator raportów, dostęp do danych i warstwa transportowa.
Segmentacja logiki podstawowej i logiki niestandardowej jest trudnym wyzwaniem. Zawsze jednak uważaliśmy, że lepiej jest mieć jeden podstawowy system z wieloma klientami, niż wiele kopii systemu dla każdego klienta.
źródło
Pracowałem na mniejszym systemie (20 kloc) i odkryłem, że DI i konfiguracja to świetne sposoby zarządzania różnicami między klientami, ale niewystarczające, aby uniknąć rozwidlenia systemu. Baza danych jest podzielona między część specyficzną dla aplikacji, która ma ustalony schemat, a część zależną od klienta, która jest zdefiniowana w niestandardowym dokumencie konfiguracyjnym XML.
Utrzymaliśmy jeden oddział w Merkurialu, który jest skonfigurowany tak, jakby był dostarczalny, ale oznakowany i skonfigurowany dla fikcyjnego klienta. Poprawki błędów są zawarte w tym projekcie, a nowy rozwój podstawowej funkcjonalności ma miejsce tylko tam. Wydania dla rzeczywistych klientów są od tego wyłączone, przechowywane we własnych repozytoriach. Śledzimy duże zmiany w kodzie poprzez ręcznie napisane numery wersji i śledzimy poprawki błędów za pomocą numerów zatwierdzeń.
źródło
Obawiam się, że nie mam bezpośredniego doświadczenia z problemem, który opisujesz, ale mam kilka uwag.
Druga opcja, polegająca na zebraniu kodu w centralnym repozytorium (w miarę możliwości) i architekturze w celu dostosowania (ponownie, w miarę możliwości) to prawie na pewno długa droga.
Problem polega na tym, jak planujesz się tam dostać i jak długo to zajmie.
W tej sytuacji prawdopodobnie (tymczasowo) posiadanie więcej niż jednej kopii aplikacji jednocześnie w repozytorium jest OK.
Umożliwi to stopniowe przejście do architektury, która bezpośrednio obsługuje dostosowywanie bez konieczności robienia tego za jednym zamachem.
źródło
Jestem pewien, że każdy z tych problemów można rozwiązać jeden po drugim. Jeśli utkniesz, zapytaj tutaj lub na SO o konkretny problem.
Jak zauważyli inni, posiadanie jednej centralnej bazy kodów / jednego repozytorium jest opcją, którą powinieneś preferować. Próbuję odpowiedzieć na twoje przykładowe pytanie.
Istnieje kilka możliwości, wszystkie widziałem w systemach rzeczywistych. Który wybrać zależy od twojej sytuacji:
wprowadzić tabele „CustomAttributes” (opisujące nazwy i typy) oraz „CustomAttributeValues” (dla wartości, na przykład przechowywanych jako ciąg znaków, nawet jeśli są liczbami). Pozwoli to dodać takie atrybuty w czasie instalacji lub w czasie wykonywania, mając indywidualne wartości dla każdego klienta. Nie nalegaj, aby każdy niestandardowy atrybut był modelowany „widocznie” w modelu danych.
teraz powinno być jasne, jak używać tego w kodzie: mieć tylko ogólny kod dostępu do tych tabel oraz indywidualny kod (być może w osobnej wtyczce DLL, która zależy od ciebie) dla poprawnej interpretacji tych atrybutów
A w przypadku konkretnego kodu: możesz także spróbować wprowadzić język skryptowy do swojego produktu, szczególnie w celu dodania skryptów specyficznych dla klienta. W ten sposób nie tylko tworzysz wyraźną linię między kodem a kodem specyficznym dla klienta, możesz również pozwolić swoim klientom na samodzielne dostosowanie systemu do pewnego stopnia.
źródło
Zbudowałem tylko jedną taką aplikację. Powiedziałbym, że 90% sprzedanych jednostek zostało sprzedanych bez zmian. Każdy klient miał własną, dostosowaną skórkę, a my obsłużyliśmy system w obrębie tej skórki. Kiedy pojawił się mod, który wpłynął na główne sekcje, próbowaliśmy użyć rozgałęzienia IF . Kiedy mod # 2 pojawił się w tej samej sekcji, przełączyliśmy się na logikę CASE, która pozwoliła na dalszą rozbudowę. Wydawało się, że poradzi sobie z większością drobnych próśb.
Wszelkie dalsze drobne niestandardowe żądania zostały obsłużone przez wdrożenie logiki Case.
Jeśli mody były dwa radykalne, zbudowaliśmy klon (osobne dołączenie) i otoczyliśmy CASE, aby uwzględnić inny moduł.
Poprawki i modyfikacje rdzenia wpłynęły na wszystkich użytkowników. Przed rozpoczęciem produkcji dokładnie przetestowaliśmy rozwój. Zawsze wysyłaliśmy powiadomienia e-mail, które towarzyszyły każdej zmianie i NIGDY, NIGDY, NIGDY nie publikowaliśmy zmian produkcyjnych w piątki ... NIGDY.
Nasze środowisko było klasyczne ASP i SQL Server. NIE byliśmy sklepem z kodami do spaghetti ... Wszystko było modułowe przy użyciu Obejmuje, podprogramów i funkcji.
źródło
Kiedy poproszę o rozpoczęcie rozwoju B, który udostępnia 80% funkcjonalności A, będę albo:
Wybrałeś 1 i wydaje się, że nie pasuje to do twojej sytuacji. Twoim zadaniem jest przewidzieć, który z 2 i 3 lepiej pasuje.
źródło