Jak mój zespół może uniknąć częstych błędów po refaktoryzacji?


20

Aby dać ci trochę tła: Pracuję dla firmy z około dwunastoma programistami Ruby on Rails (stażyści +/-). Praca zdalna jest powszechna. Nasz produkt składa się z dwóch części: raczej grubego rdzenia i zbudowanych na nim dużych projektów klientów. Projekty klientów zwykle rozszerzają rdzeń. Zastąpienie kluczowych funkcji nie występuje. Mógłbym dodać, że rdzeń ma kilka dość złych części, które pilnie wymagają refaktoryzacji. Istnieją specyfikacje, ale głównie dla projektów klientów. Najgorsza część rdzenia jest nieprzetestowana (nie tak, jak powinna być ...).

Programiści są podzieleni na dwa zespoły, pracujące z jednym lub dwoma PO dla każdego sprintu. Zwykle jeden projekt klienta jest ściśle powiązany z jednym z zespołów i organizacji producentów.

Teraz nasz problem: raczej często psujemy się nawzajem. Ktoś z Zespołu A rozszerza lub refaktoryzuje podstawową funkcję Y, powodując nieoczekiwane błędy w jednym z projektów klientów Zespołu B. Przeważnie zmiany nie są ogłaszane przez zespoły, więc błędy trafiają prawie zawsze nieoczekiwanie. Zespół B, w tym PO, uważał, że funkcja Y jest stabilna i nie wypróbował jej przed wydaniem, nieświadomy zmian.

Jak pozbyć się tych problemów? Jaką „technikę ogłoszenia” możesz mi polecić?


34
Oczywistą odpowiedzią jest TDD .
mouviciel

1
Jak można stwierdzić, że „nadpisywanie kluczowych funkcji się nie zdarza”, a problem polega na tym, że tak się dzieje? Czy w swoim zespole rozróżniasz „kluczowe” i „kluczowe funkcje” i jak to robisz? Próbuję tylko zrozumieć sytuację ...
logc

4
@mouvciel To i nie używaj dynamicznego pisania , ale ta konkretna rada jest w tym przypadku nieco za późno.
Doval

3
Użyj silnie napisanego języka, takiego jak OCaml.
Gajusz

@logc Być może nie było jasne, przepraszam. Nie zastępujemy podstawowej funkcji, takiej jak sama biblioteka filtrów, ale dodajemy nowe filtry do klas, których używamy w naszych projektach klienckich. Jednym z typowych scenariuszy może być to, że zmiany w bibliotece filtrów niszczą dodane filtry w projekcie klienta.
SDD64,

Odpowiedzi:


24

Polecam lekturę Working Effective with Legacy Code autorstwa Michaela C. Feathersa . Wyjaśnia, że ​​naprawdę potrzebujesz automatycznych testów, jak możesz je łatwo dodać, jeśli jeszcze ich nie masz, i jaki „zapach śmierdzi”, aby refaktoryzować w jaki sposób.

Poza tym kolejnym zasadniczym problemem w twojej sytuacji wydaje się brak komunikacji między dwoma zespołami. Jak duże są te zespoły? Czy pracują na różnych zaległościach?

Prawie zawsze złą praktyką jest dzielenie zespołów w zależności od architektury. Np. Zespół podstawowy i zespół inny niż podstawowy. Zamiast tego tworzyłbym zespoły w domenie funkcjonalnej, ale wieloskładnikowej.


Przeczytałem w „The Mythical Man-Month”, że struktura kodu zwykle podąża za strukturą zespołu / organizacji. Tak więc nie jest to tak naprawdę „zła praktyka”, ale po prostu sposób, w jaki zwykle wszystko idzie.
Marcel

Myślę, że w „ Dynamice rozwoju oprogramowania ” menedżer Visual C ++ zdecydowanie zaleca posiadanie zespołów funkcyjnych; Nie przeczytałem „The Mythical Man-Month”, @Marcel, ale AFAIK wymienia złe praktyki w branży ...
logc

Marcel, prawdą jest, że tak zwykle się dzieje lub poszło, ale coraz więcej zespołów robi to inaczej, np. Zespoły fabularne. Posiadanie zespołów opartych na komponentach powoduje brak komunikacji podczas pracy nad funkcjami między komponentami. Poza tym prawie zawsze doprowadzi to do dyskusji architektonicznych nie opartych na celu dobrej architektury, ale ludzi próbujących przekazać obowiązki innym zespołom / komponentom. Otrzymasz zatem sytuację opisaną przez autora tego pytania. Zobacz także mountaingoatsoftware.com/blog/the-benefits-of-feature-teams .
Tohnmeister

O ile rozumiem PO, stwierdził, że zespoły nie są podzielone na zespół podstawowy i inny niż podstawowy. Zespoły są podzielone „na klienta”, co zasadniczo jest „na domenę funkcjonalną”. I to jest część problemu: ponieważ wszystkie zespoły mogą zmieniać wspólny rdzeń, zmiany z jednego zespołu wpływają na drugi.
Doc Brown

@DocBrown Masz rację. Każda drużyna może zmienić rdzeń. Oczywiście zmiany te powinny być korzystne dla każdego projektu. Działają jednak na różnych zaległościach. Mamy jeden dla każdego klienta i jeden dla rdzenia.
SDD64,

41

Najgorsza część rdzenia nie jest testowana (tak jak powinno być ...).

To jest problem. Wydajne refaktoryzacja zależy w dużej mierze od zestawu zautomatyzowanych testów. Jeśli ich nie masz, zaczynają się pojawiać problemy, które opisujesz. Jest to szczególnie ważne, jeśli używasz dynamicznego języka, takiego jak Ruby, gdzie nie ma kompilatora do wychwytywania podstawowych błędów związanych z przekazywaniem parametrów do metod.


10
To i refaktoryzacja w krokach dziecka i popełnienie bardzo często.
Stefan Billiet

1
Prawdopodobnie istnieje mnóstwo porad, które mogłyby tu dodać porady, ale wszystko sprowadza się do tego punktu. Niezależnie od tego, że OP „żartuje tak, jak powinien”, dowodząc, że wiedzą, że to problem sam w sobie, wpływ testowania skryptowego na refaktoryzację jest ogromny: jeśli przepustka się nie powiedzie, refaktoryzacja nie zadziała. Jeśli wszystkie przejścia pozostaną przejściami, wówczas refaktoryzacja mogłaby zadziałać (przeniesienie nieudanych przejść byłoby oczywiście plusem, ale utrzymanie wszystkich przebiegów jako przejścia jest ważniejsze niż nawet zysk netto; zmiana, która przerywa jeden test i naprawia pięć, może być poprawa, ale nie refaktoryzacja)
Jon Hanna

Dałem ci „+1”, ale myślę, że „testy automatyczne” to nie jedyne podejście do rozwiązania tego problemu. Lepsza ręczna, ale systematyczna kontrola jakości, być może przez oddzielny zespół kontroli jakości może rozwiązać problemy z jakością (i prawdopodobnie sensowne jest posiadanie zarówno testów automatycznych, jak i ręcznych).
Doc Brown

Dobra uwaga, ale jeśli projekty rdzenia i klienta są oddzielnymi modułami (a ponadto w dynamicznym języku, takim jak Ruby), wówczas rdzeń może zmienić zarówno test, jak i powiązaną z nim implementację , i przerwać moduł zależny bez niepowodzenia własnych testów.
logc

Jak skomentowali inni. TDD. Prawdopodobnie już wiesz, że powinieneś mieć testy jednostkowe dla jak największej części kodu. Podczas pisania testów jednostkowych tylko ze względu na to marnowanie zasobów, kiedy zaczynasz refaktoryzować dowolny komponent, powinieneś zacząć od obszernego pisania testów przed dotknięciem podstawowego kodu.
jb510

5

Poprzednie odpowiedzi wskazujące na lepsze testy jednostkowe są dobre, ale uważam, że mogą być bardziej podstawowe kwestie do rozwiązania. Potrzebujesz przejrzystych interfejsów, aby uzyskać dostęp do kodu podstawowego z kodu dla projektów klientów. W ten sposób, jeśli refaktoryzujesz kod podstawowy bez zmiany zachowania obserwowanego przez interfejsy , kod drugiego zespołu nie ulegnie uszkodzeniu. Dzięki temu znacznie łatwiej będzie wiedzieć, co można „bezpiecznie” zreformować, a co wymaga przeprojektowania, być może zepsucia interfejsu.


Spot on. Bardziej zautomatyzowane testowanie przyniesie same korzyści i jest całkowicie warte zrobienia, ale nie rozwiąże tutaj podstawowego problemu, jakim jest brak komunikacji podstawowych zmian. Oddzielenie poprzez owijanie interfejsów wokół ważnych funkcji będzie ogromnym ulepszeniem.
Bob Tway,

5

Inne odpowiedzi uwypukliły ważne punkty (więcej testów jednostkowych, zespołów funkcji, czyste interfejsy do podstawowych komponentów), ale brakuje mi jednego punktu, jakim jest wersjonowanie.

Jeśli zamrozisz zachowanie swojego rdzenia, wykonując wydanie 1 i umieścisz to wydanie w prywatnym systemie zarządzania artefaktami 2 , wówczas każdy projekt klienta może zadeklarować swoją zależność od wersji podstawowej X i nie zostanie zepsuty w następnej wersji X + 1 .

„Zasady ogłaszania” ograniczają się do posiadania pliku ZMIANY wraz z każdym wydaniem lub spotkania zespołu, aby ogłosić wszystkie funkcje każdego nowego wydania podstawowego.

Ponadto uważam, że musisz lepiej zdefiniować, co jest „rdzeniem”, a jaki jego podzbiór to „klucz”. Wydaje się (poprawnie) unikać wprowadzania wielu zmian w „kluczowych komponentach”, ale pozwalasz na częste zmiany w „rdzeniu”. Aby na czymś polegać, musisz zachować stabilność; jeśli coś nie jest stabilne, nie nazywaj tego rdzeniem. Może mógłbym zasugerować nazywanie go komponentami pomocniczymi?

EDYCJA : Jeśli postępujesz zgodnie z konwencjami w systemie wersjonowania semantycznego , każda niekompatybilna zmiana w interfejsie API rdzenia musi być oznaczona poważną zmianą wersji . Oznacza to, że gdy zmienisz zachowanie istniejącego rdzenia lub usuniesz coś, nie tylko dodasz coś nowego. Dzięki tej konwencji programiści wiedzą, że aktualizacja z wersji „1.1” do „1.2” jest bezpieczna, ale przejście z wersji „1.X” do „2.0” jest ryzykowne i należy ją uważnie przejrzeć.

1: Myślę, że nazywa się to klejnotem w świecie Ruby
2: odpowiednik Nexusa w Javie lub PyPI w Pythonie


„Wersjonowanie” jest wprawdzie ważne, ale kiedy ktoś próbuje rozwiązać opisany problem poprzez zamrożenie rdzenia przed wydaniem, wtedy łatwo kończy się potrzeba wyrafinowanego rozgałęziania i łączenia. Powodem jest to, że w fazie „kompilacji wydania” zespołu A, A może musieć zmienić rdzeń (przynajmniej w celu naprawy błędów), ale nie zaakceptuje zmian w rdzeniu innych zespołów - więc kończysz na jednej gałęzi rdzeń na zespół, który ma zostać połączony „później”, co jest formą długu technicznego. Czasami jest to w porządku, ale często po prostu odsuwa opisany problem na później.
Doc Brown

@DocBrown: Zgadzam się z tobą, ale napisałem przy założeniu, że wszyscy programiści są spółdzielni i dorośli. Nie oznacza to, że nie widziałem tego, co opisujesz . Ale kluczową częścią niezawodności systemu jest, cóż, dążenie do stabilności. Ponadto, jeśli zespół A musi zmienić X w rdzeniu, a zespół B musi zmienić X w rdzeniu, to może X nie należy do rdzenia; Myślę, że to mój drugi punkt. :)
logc

@DocBrown Tak, nauczyliśmy się używać jednego oddziału rdzenia dla każdego projektu klienta. To spowodowało inne problemy. Na przykład nie lubimy „dotykać” już wdrożonych systemów klientów. W rezultacie po każdym wdrożeniu mogą napotkać kilka drobnych skoków wersji używanych rdzeni.
SDD64,

@ SDD64: dokładnie to mówię - niezwłoczne zintegrowanie zmian we wspólnym rdzeniu nie jest rozwiązaniem w dłuższej perspektywie. Potrzebna jest lepsza strategia testowania rdzenia - z automatycznymi i ręcznymi testami.
Doc Brown

1
Dla przypomnienia, nie opowiadam się za oddzielnym rdzeniem dla każdego zespołu ani nie zaprzeczam, że testy są wymagane - ale test podstawowy i jego implementacja mogą ulec zmianie w tym samym czasie, jak już wcześniej skomentowałem . Projekt oparty na nim może polegać tylko na zamrożonym rdzeniu, oznaczonym ciągiem zwalniającym lub znacznikiem zatwierdzenia (z wyłączeniem poprawek błędów i pod warunkiem, że zasady kontroli wersji są prawidłowe).
logc

3

Jak powiedzieli inni ludzie, dobry zestaw testów jednostkowych nie rozwiąże twojego problemu: będziesz mieć problem z scalaniem zmian, nawet jeśli każdy zespół testowy przejdzie pomyślnie.

To samo dotyczy TDD. Nie wiem, jak to rozwiązać.

Twoje rozwiązanie jest nietechniczne. Musisz jasno zdefiniować granice „rdzenia” i przypisać komuś rolę „stróżującego psa”, niezależnie od tego, czy jest to główny projektant, czy architekt. Wszelkie zmiany rdzenia muszą przejść przez ten organ nadzorczy. Jest odpowiedzialny za to, aby wszystkie wyniki wszystkich zespołów zostały połączone bez nadmiernych szkód dodatkowych.


Mieliśmy „psa stróżującego”, ponieważ napisał większość rdzenia. Niestety był również odpowiedzialny za większość nie przetestowanych części. Został podszyty pod YAGNI i został zastąpiony pół roku temu przez dwóch innych facetów. Nadal staramy się refaktoryzować te „ciemne części”.
SDD64,

2
Chodzi o to, aby mieć zestaw testów jednostkowych dla rdzenia , który jest częścią rdzenia , z udziałem wszystkich zespołów, a nie oddzielnych zestawów testów dla każdego zespołu.
Doc Brown

2
@ SDD64: wydaje się, że mylisz „Nie będziesz tego potrzebował (jeszcze)” (co jest bardzo dobrą rzeczą) z „Nie musisz już oczyszczać swojego kodu (jeszcze)” - co jest wyjątkowo złym nawykiem i IMHO wręcz przeciwnie.
Doc Brown

Rozwiązanie watchdog jest naprawdę, bardzo nieoptymalne, IMHO. To jest jak budowanie pojedynczego punktu awarii w twoim systemie, a ponadto bardzo powolnego, ponieważ dotyczy osoby i polityki. W przeciwnym razie TDD może oczywiście pomóc w rozwiązaniu tego problemu: każdy test rdzenia jest przykładem dla deweloperów projektu klienta, w jaki sposób należy wykorzystać bieżący rdzeń. Ale myślę, że udzieliłeś odpowiedzi w dobrej wierze ...
logc

@DocBrown: OK, może nasze rozumienie jest inne. Napisane przez niego podstawowe funkcje są zbyt skomplikowane, aby zaspokoić nawet najdziwniejsze możliwości. Większość z nich nigdy się nie spotkaliśmy. Z drugiej strony złożoność spowalnia nas do refaktoryzacji.
SDD64,

2

Jako długoterminową poprawkę potrzebujesz także lepszej i terminowej komunikacji między zespołami. Każdy zespół, który kiedykolwiek wykorzysta, na przykład podstawową funkcję Y, musi być zaangażowany w budowę planowanych testów dla tej funkcji. Planowanie samo w sobie uwypukli różne przypadki użycia związane z funkcją Y między dwoma zespołami. Po ustaleniu sposobu działania funkcji i wdrożeniu i uzgodnieniu przypadków testowych wymagana jest dodatkowa zmiana w schemacie implementacji. Zespół wypuszczający tę funkcję jest wymagany do uruchomienia zestawu testowego, a nie zespół, który zamierza go użyć. Zadaniem, które powinno spowodować kolizje, jest dodanie nowej skrzynki testowej od jednego z zespołów. Gdy członek zespołu pomyśli o nowym aspekcie funkcji, która nie jest testowana, powinni mieć swobodę dodawania testowej skrzynki, którą zweryfikowali, przekazując do swojej piaskownicy. W ten sposób jedyne kolizje, które będą miały miejsce, będą na zamierzonym poziomie i powinny zostać przybite, zanim zmieniona funkcja zostanie wypuszczona na wolność.


2

Chociaż każdy system potrzebuje skutecznych pakietów testowych (co oznacza między innymi automatyzację) i chociaż testy te, jeśli są stosowane skutecznie, wychwytują te konflikty wcześniej niż są teraz, nie rozwiązuje to podstawowych problemów.

Pytanie ujawnia co najmniej dwa podstawowe problemy: praktykę modyfikowania „rdzenia” w celu spełnienia wymagań dla indywidualnych klientów oraz brak komunikacji między zespołami i koordynacji ich zamiaru wprowadzenia zmian. Żadna z tych przyczyn nie jest podstawowa i zanim będzie można to naprawić, musisz zrozumieć, dlaczego tak się dzieje.

Jedną z pierwszych rzeczy, które należy ustalić, jest to, czy zarówno programiści, jak i menedżerowie zdają sobie sprawę, że jest tutaj problem. Jeśli przynajmniej niektórzy tak robią, musisz dowiedzieć się, dlaczego albo myślą, że nic nie mogą z tym zrobić, albo nie. Dla tych, którzy tego nie robią, możesz spróbować zwiększyć ich zdolność do przewidywania, w jaki sposób ich obecne działania mogą powodować przyszłe problemy, lub zastąpić ich ludźmi, którzy potrafią. Dopóki nie będziesz mieć siły roboczej, która jest świadoma tego, co się dzieje źle, prawdopodobnie nie będziesz w stanie rozwiązać problemu (a być może nawet wtedy, przynajmniej w krótkim okresie).

Analiza problemu może być trudna w kategoriach abstrakcyjnych, przynajmniej początkowo, więc skup się na konkretnym incydencie, który spowodował problem, i spróbuj ustalić, jak to się stało. Ponieważ zaangażowane osoby prawdopodobnie zachowują się defensywnie, musisz być czujny na egoistyczne i post-hoc uzasadnienia, aby dowiedzieć się, co się naprawdę dzieje.

Jest jedna możliwość, o której waham się wspomnieć, ponieważ jest to tak mało prawdopodobne: wymagania klientów są tak zróżnicowane, że nie ma wystarczającej podobieństwa, aby uzasadnić wspólny kod podstawowy. Jeśli tak, to faktycznie masz wiele oddzielnych produktów i powinieneś nimi zarządzać, a nie tworzyć między nimi sztucznego połączenia.


Zanim przeprowadziliśmy migrację naszego produktu z Javy do RoR, faktycznie działaliśmy tak, jak sugerowałeś. Jeden z nas miał rdzeń Java dla wszystkich klientów, ale ich wymagania „złamały” go pewnego dnia i musieliśmy go podzielić. W tej sytuacji napotkaliśmy takie problemy, jak: „Stary, klient Y ma tak fajną podstawową funkcję. Szkoda, że ​​nie możemy przenieść go do klienta Z, ponieważ ich rdzeń jest niezgodny ”. W przypadku Railsów zdecydowanie chcemy stosować zasadę „jeden rdzeń dla wszystkich”. Jeśli tak musi być, nadal oferujemy drastyczne zmiany, ale te odciągają klienta od wszelkich dalszych aktualizacji.
SDD64

Samo dzwonienie do TDD wydaje mi się niewystarczające. Tak więc, oprócz podziału głównej sugestii, najbardziej podoba mi się twoja odpowiedź. Niestety rdzeń nie jest idealnie przetestowany, ale to nie rozwiązałoby wszystkich naszych problemów. Dodanie nowych podstawowych funkcji dla jednego klienta może wydawać się całkowicie w porządku, a nawet dać im zieloną wersję, ponieważ tylko podstawowe specyfikacje są dzielone między klientami. Nie można zauważyć, co dzieje się z każdym możliwym klientem. Podoba mi się twoja sugestia, aby dowiedzieć się o problemach i porozmawiać o tym, co je spowodowało.
SDD64,

1

Wszyscy wiemy, że należy przejść testy jednostkowe. Ale wiemy również, że realistyczne dopasowanie ich do rdzenia jest trudne.

Specjalną techniką, która może być przydatna podczas rozszerzania funkcjonalności, jest próba tymczasowego i lokalnego sprawdzenia, czy istniejąca funkcjonalność nie została zmieniona. Można to zrobić w następujący sposób:

Oryginalny pseudo kod:

def someFunction
   do original stuff
   return result
end

Tymczasowy kod testowy na miejscu:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

Uruchom tę wersję, niezależnie od istniejących testów na poziomie systemu. Jeśli wszystko jest w porządku, wiesz, że nic nie zepsułeś, i możesz następnie usunąć stary kod. Pamiętaj, że po sprawdzeniu, czy stare i nowe wyniki są zgodne, możesz również dodać kod do analizy różnic, aby uchwycić przypadki, które, jak wiesz, powinny być różne z powodu zamierzonej zmiany, takiej jak naprawa błędu.


1

„Przeważnie zmiany nie są ogłaszane przez zespoły, więc błędy trafiają prawie zawsze nieoczekiwanie”

Masz problem z komunikacją? Co (oprócz tego, co wszyscy inni już zauważyli, że powinieneś być rygorystycznym testowaniem) upewniając się, że istnieje właściwa komunikacja? Że ludzie są świadomi, że interfejs, do którego piszą, zmieni się w następnej wersji i jakie będą te zmiany?
I zapewnij im dostęp do co najmniej fałszywego interfejsu (z pustą implementacją) tak szybko, jak to możliwe podczas programowania, aby mogli zacząć pisać własny kod.

Bez tego testy jednostkowe niewiele by zrobiły, poza tym, że w końcowych etapach stwierdzono, że między częściami systemu coś jest nie do zniesienia. Chcesz to wiedzieć, ale chcesz to wiedzieć wcześnie, bardzo wcześnie, a zespoły rozmawiają ze sobą, koordynują wysiłki i faktycznie mają częsty dostęp do pracy wykonywanej przez drugi zespół (tak więc regularnie się zobowiązuje, a nie jednego masywnego popełnić po kilku tygodniach lub miesiącach, 1-2 dni przed dostawą).
Twój błąd NIE znajduje się w kodzie, a na pewno nie w kodzie innego zespołu, który nie wiedział, że zadzierasz z interfejsem, przed którym piszą. Twój błąd jest w procesie rozwoju, braku komunikacji i współpracy między ludźmi. To, że siedzisz w różnych pokojach, nie oznacza, że ​​powinieneś izolować się od innych facetów.


1

Przede wszystkim masz problem z komunikacją (prawdopodobnie również związany z problemem budowania zespołu ), więc myślę, że rozwiązanie twojej sprawy powinno koncentrować się na ... no cóż, komunikacji zamiast na technikach programistycznych.

Przyjmuję za pewnik, że nie można zamrozić ani rozwidlić modułu podstawowego podczas rozpoczynania projektu klienta (w przeciwnym razie wystarczy po prostu zintegrować z harmonogramem firmy niektóre projekty niezwiązane z klientem, których celem jest aktualizacja modułu podstawowego).

Pozostaje nam więc problem poprawy komunikacji między zespołami. Można temu zaradzić na dwa sposoby:

  • z ludźmi. Oznacza to, że Twoja firma wyznaczy kogoś na głównego architekta modułu (lub jakikolwiek żargon jest dobry dla najwyższego kierownictwa), który będzie odpowiedzialny za jakość i dostępność kodu. Ta osoba wcieli się w rdzeń. Dzięki temu będzie współdzielona przez wszystkie zespoły i zapewni odpowiednią synchronizację między nimi. Ponadto powinna również pełnić rolę recenzenta kodu przydzielonego modułowi podstawowemu, aby zachować jego spójność;
  • z narzędziami i przepływami pracy. Poprzez nałożenie Continuous Integration na rdzeń, sam kod rdzeniowy stanie się medium komunikacyjnym. Będzie to wymagało najpierw wysiłku (poprzez dodanie do niego zautomatyzowanych zestawów testowych), ale następnie nocne raporty CI będą aktualizacją statusu brutto modułu podstawowego.

Więcej informacji o CI jako procesie komunikacji można znaleźć tutaj .

W końcu masz problem z brakiem pracy zespołowej na poziomie firmy. Nie jestem wielkim fanem imprez integracyjnych, ale wydaje się, że byłyby przydatne. Czy regularnie organizujesz spotkania dla programistów? Czy możesz zaprosić osoby z innych zespołów do retrospekcji projektu? A może masz czasem piwo w piątek wieczorem?

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.