Najlepsze praktyki dla wspierających rozwój silnika

Wprowadzenie

Godot posiada dużą ilość użytkowników, którzy są w stanie go rozwijać, zważywszy na to, że projekt sam w sobie jest skierowany głównie do użytkowników z umiejętnością programowania. Mimo to, nie wszyscy są na tym samym poziomie doświadczenia w pracy z dużymi projektami lub w inżynierii oprogramowania, co może prowadzić do częstych nieporozumień i złych praktyk podczas rozwijania kodu projektu.

Język

Zakres tego dokumentu to lista najlepszych praktyk do zastosowania dla rozwijających kod, jak i do tworzenie języka, którego mogą używać w odniesieniu do powszechnych sytuacji, które powstają w procesie oddawania ich zmian.

Podczas, gdy niektórzy mogą uznać za pożyteczne rozszerzenie tego na ogólne wytwarzanie oprogramowania, naszym zamiarem jest ograniczenie się tylko do sytuacji najczęściej spotykanych w naszym projekcie.

Wsparcie najczęściej jest klasyfikowane jako naprawa błędów, ulepszenia lub nowe funkcjonalności. By uogólnić to pojęcie, nazwiemy je Rozwiązaniami, ponieważ zawsze starają się rozwiązać coś, co można określić jako Problem.

Najlepsze praktyki

#1: Problem jest zawsze na pierwszym miejscu

Wielu wspierających jest niesamowicie kreatywnych i po prostu uwielbiaja proces projektowania abstrakcyjnych struktur danych, tworzenia ładnych interfejsów użytkowania, lub po prostu kocha programować. Jakkolwiek by nie było, wpadają na świetne pomysły, które mogą, ale wcale nie muszą rozwiązywać jakichkolwiek rzeczywistych problemów.

../../_images/best_practices1.png

Zwykle nazywa się je Rozwiązaniami w poszukiwaniu problemu. W idealnym świecie nie byłyby one szkodliwe, jednak w rzeczywistości kod wymaga czasu na napisanie, zajmuje miejsce jako źródło i binaria oraz wymaga utrzymywania, gdy już będzie istniał. Unikanie dodawania czegokolwiek, co niepotrzebne, w rozwoju oprogramowania zawsze jest uznawane za dobrą praktykę.

#2: By rozwiązać problem, najpierw musi on istnieć

Jest to odmiana poprzedniej praktyki. Dodawanie czegokolwiek niepotrzebnego nie jest dobrym pomysłem, ale co stanowi o tym, co jest potrzebne a co nie?

../../_images/best_practices2.png

Odpowiedzią na to pytanie jest to, że problem, zanim zostanie faktycznie rozwiązany, musi istnieć. Nie może być to spekulacja lub przekonanie. Użytkownik musi używać oprogramowania zgodnie z przeznaczeniem aby utworzyć coś, czego potrzebuje. W procesie tym, użytkownik może się natknąć na jakiś problem, który wymaga rozwiązania w celu przejścia przez niego, lub w celu osiągnięcia większej produktywności. W tym wypadku, potrzeba rozwiązania.

Wiara w to, że w przyszłości mogą powstać problemy, oraz że oprogramowanie musi być gotowe do ich rozwiązania zanim się pojawią nazywa się "Zabezpieczeniem na przyszłość" i charakteryzuje się sposobami myślenia takimi jak:

  • Myślę, że użytkownikom przydałoby się...

  • Myślę, że koniec końców użytkownicy będą potrzebować...

Ogólnie uważa się to za zły nawyk, ponieważ próba rozwiązania problemów, które obecnie właściwie nie istnieją, często prowadzi do powstania kodu, który będzie napisany ale nigdy nie będzie użyty, lub który jest znacznie bardziej skomplikowany w użyciu i utrzymaniu niż powinien być.

#3: Problem musi być złożony lub częsty

Oprogramownaie jest zaprojektowane w celu rozwiązania problemów, ale nie możemy spodziewać się rozwiązania każdego problemu, jaki tylko istnieje pod słońcem. Godot, jako silnik gry, rozwiąże za Ciebie problemy, pomoże Ci więc robić gry lepiej i szybciej, ale nie zrobi całej gry za Ciebie. Gdzieś trzeba poprowadzić granicę.

../../_images/best_practices3.png

To, czy problem jest warty rozwiązywania, określone jest przez trud, z jakim użytkownik musi nad nim pracować. Trud ten można wyrazić jako:

  • Złożoność problemu

  • Częstotliwość występowania problemu

Jeśli problem jest zbyt złożony do rozwiązania dla większości użytkowników, to oprogramownaie musi oferować przygotowane na niego rozwiązanie. Podobnie, jeśli problem jest dla użytkowników łatwy do obejścia, oferowanie takiego rozwiązania jest niepotrzebne i to do użytkownika należy jego opracowanie.

Wyjątkiem jest jednak, kiedy użytkownik natyka się na ten problem na tyle często, że konieczność robienia za każdym razem prostego rozwiązania staje się irytująca. W tym wypadku, oprogramowanie musi oferować rozwiązanie do uproszczenia tego przypadku użycia.

Z naszego doświadczenia, w większości przypadków rozróżnienie, kiedy problem jest złożony lub częsty jest oczywiste, ale mogą powstać przypadki, w których poprowadzenie tej linii jest trudne. To dlatego zawsze zaleca się dyskusje z innymi deweloperami (następny punkt).

#4: Rozwiązanie musi być przedyskutowane z innymi

To częsty przypadek, że kiedy użytkownicy natykają się na problemy, są zanurzeni tylko w swoim projekcie, więc naturalnie próbują rozwiązać ten problem ze swojej perspektywy, myśląc wyłącznie o swoim przypadku użycia.

Z tego powodu, rozwiązania zaproponowane przez użytkownika nie zawsze biorą pod uwagę inne przypadki wykorzystania, których inni deweloperzy są świadomi, więc często są stronniczy na rzecz swoich własnych wymagań.

../../_images/best_practices4.png

Dla deweloperów, perspektywa jest inna. Mogą uważać problem użytkownika za zbyt wyjątkowy, by uzasadnić rozwiązanie (zamiast obejścia przez użytkownika), albo może zasugerują częściowe (zazwyczaj prostsze lub na niższym poziomie) rozwiązanie, które ma zastosowanie do szerszego zakresu znanych problemów, a resztę rozwiązania pozostawią użytkownikowi.

W każdym przypadku, przed porwaniem się na wsparcie, ważne jest, aby przedyskutować rzeczywiste problemy z innymi deweloperami lub wspierającymi, dzięki czemu można osiągnąć lepsze porozumienie w sprawie implementacji.

Jedyny wyjątek, w tym przypadku, jest wtedy, kiedy jakiś obszar kodu ma wyraźnego właściciela (uzgodnionego przez innych wspierających), który rozmawia bezpośrednio z użytkownikami i ma największą wiedzę, aby bezpośrednio zaimplementować rozwiązanie.

Filozofią Godota jest, również, faworyzować łatwość użycia i utrzymania ponad absolutną wydajność. Optymalizacje wydajności będą wzięte pod uwagę, ale nie mogą być zaakceptowane, jeśli czynią coś zbyt trudnym w użyciu lub dodają zbyt dużo zawiłości do bazy kodu.

#5: Do każdego problemu, jego własne rozwiązanie

For programmers, it is always a most enjoyable challenge to find the most optimal solutions to problems. Things, however, may go overboard sometimes and programmers will try to come up with solutions that solve as many problems as possible.

The situation will often take a turn for the worse when, in order to make this solution appear even more fantastic and flexible, the pure speculation-based problems (as described in #2) also make their appearance on stage.

../../_images/best_practices5.png

The main problem is that, in reality, it rarely works this way. Most of the time, writing an individual solution to each problem results in code that is simpler and more maintainable.

Additionally, solutions that target individual problems are better for the users, as they find something that does exactly what they need, without having to learn and remember a more complex system they will only need for simple tasks.

Big and flexible solutions also have an additional drawback which is that, over time, they are rarely flexible enough for all users, who keep requesting more functions added (and making the API and codebase more and more complex).

#6: Cater to common use cases, leave the door open for the rare ones

This is a continuation of the previous point, which further explains why this way of thinking and designing software is preferred.

As mentioned before (in point #2), it is very difficult for us (as human beings who design software) to actually understand all future user needs. Trying to write very flexible structures that cater to many use cases at once is often a mistake.

We may come up with something we believe is brilliant, but when it's actually used, we will find that users will never even use half of it, or that they will require features that don't quite accommodate our original design, forcing us to either throw it away or make it even more complex.

The question is then, how to design software that gives users what we know they need, but that is flexible enough to allow them to do what we don't know they might need in the future?

../../_images/best_practices6.png

The answer to this question is that, to ensure users still can do what they want to do, we need to give them access to a low level API that they can use to achieve what they want, even if it's more work for them because it means reimplementing some logic that already exists.

In real-life scenarios, these use cases will be at most rare and uncommon anyway, so it makes sense a custom solution needs to be written. This is why it's important to still provide users the basic building blocks to do it.

#7: Prefer local solutions

When looking for a solution to a problem, be it implementing a new feature or fixing a bug, sometimes the easiest path is to add data or a new function in the core layers of code.

The main problem here is, adding something to the core layers that will only be used from a single location far away will not only make the code more difficult to follow (split in two), but also make the core API larger, more complex, more difficult to understand in general.

This is bad, because readability and cleanness of core APIs is always of extreme importance given how much code relies on it, and because it's key for new contributors as a starting point to learning the codebase.

../../_images/best_practices7.png

The common reasoning for wanting to do this is that it's usually less code to simply add a hack in the core layers.

Despite this, this practice is not advised. Generally, the code for a solution should be closer to where the problem originates, even if it involves more code, duplicated, more complex or is less efficient. More creativity might be needed, but this path is always the advised one.

#8: Don't use complex canned solutions for simple problems

Not every problem has a simple solution and, many times, the right choice is to use a third party library to solve the problem.

As Godot requires to be shipped in a large amount of platforms, we can't link libraries dynamically. Instead, we bundle them in our source tree.

../../_images/best_practices8.png

As a result, we are very picky with what goes in, and we tend to prefer smaller libraries (in fact, single header ones are our favorite). Only in cases where there is no other choice we end up bundling something larger.

Also, libraries must use a permissive enough license to be included into Godot. Some examples of acceptable licenses are Apache 2.0, BSD, MIT, ISC, and MPL 2.0. In particular, we cannot accept libraries licensed under the GPL or LGPL since these licenses effectively disallow static linking in proprietary software (which Godot is distributed as in most exported projects). This requirement also applies to the editor, since we may want to run it on iOS in the long term. Since iOS doesn't support dynamic linking, static linking the only option on that platform.