To trochę ezoteryczne co najmniej, jak już zauważyłeś, co może powodować, że drapię się po głowie, gdy zaczynam napotykać twój kod, zastanawiając się, co robisz i gdzie te klasy pomocnicze są wdrażane, dopóki nie zacznę wybierać twojego stylu / nawyki (w tym momencie mogę się do tego całkowicie przyzwyczaić).
Podoba mi się, że zmniejszasz ilość informacji w nagłówkach. Zwłaszcza w bardzo dużych bazach kodowych, które mogą mieć praktyczne skutki w celu zmniejszenia zależności od czasu kompilacji i ostatecznie czasu kompilacji.
Moją reakcją jest jednak to, że jeśli czujesz potrzebę ukrywania szczegółów implementacji w ten sposób, aby faworyzować przekazywanie parametrów do funkcji wolnostojących z wewnętrznym łączeniem w pliku źródłowym. Zwykle możesz zaimplementować funkcje narzędziowe (lub całe klasy) przydatne do zaimplementowania określonej klasy bez dostępu do wszystkich elementów wewnętrznych klasy i zamiast tego po prostu przekazać odpowiednie z implementacji metody do funkcji (lub konstruktora). Oczywiście ma to tę zaletę, że zmniejsza sprzężenie między klasą a „pomocnikami”. Ma również tendencję do uogólniania, co w innym przypadku mogłoby być „pomocnikami”, jeśli okaże się, że zaczynają one służyć bardziej ogólnemu celowi, stosowanemu w więcej niż jednej implementacji klasy.
Czasem też trochę się denerwuję, gdy widzę w kodzie mnóstwo „pomocników”. Nie zawsze jest to prawdą, ale czasami mogą być symptomatyczne dla programisty, który po prostu rozkłada funkcje chcąc nie chcąc, aby wyeliminować powielanie kodu z ogromnymi plamami danych przekazywanymi do funkcji o ledwo zrozumiałych nazwach / celach poza faktem, że zmniejszają one ilość kod wymagany do implementacji niektórych innych funkcji. Tylko odrobinę więcej przemyślenia może czasami prowadzić do znacznie większej przejrzystości, jeśli chodzi o sposób, w jaki implementacja klasy jest rozkładana na dalsze funkcje, i faworyzowanie przekazywania określonych parametrów do przekazywania całych instancji obiektu z pełnym dostępem do elementów wewnętrznych może pomóc promować ten styl myślenia projektowego. Oczywiście nie sugeruję, że to robisz (nie mam pojęcia),
Jeśli stanie się to niewygodne, rozważę drugie, bardziej idiomatyczne rozwiązanie, którym jest pimpl (zdaję sobie sprawę, że wspomniałeś o problemach, ale myślę, że możesz uogólnić rozwiązanie, aby uniknąć tych przy minimalnym wysiłku). Może to przenieść wiele informacji, które Twoja klasa musi wdrożyć, w tym prywatne dane, od hurtowego nagłówka. Problemy z wydajnością pimpl można w dużej mierze złagodzić za pomocą taniego taniego alokatora czasu stałego *, takiego jak darmowa lista, przy jednoczesnym zachowaniu semantyki wartości bez konieczności implementowania pełnej definicji ctor kopiowania zdefiniowanej przez użytkownika.
- Jeśli chodzi o wydajność, pimpl wprowadza co najmniej wskaźnik nad głową, ale myślę, że przypadki muszą być dość poważne, gdy stanowi praktyczną troskę. Jeśli położenie przestrzenne nie ulegnie znacznej degradacji za pomocą alokatora, wówczas ciasne pętle iterujące po obiekcie (które powinny być na ogół jednorodne, jeśli wydajność jest bardzo istotna) nadal będą miały tendencję do minimalizowania braków pamięci podręcznej w praktyce, pod warunkiem, że użyjesz czegoś takiego darmowa lista do przydzielenia pimpl, umieszczająca pola klasy w przeważnie ciągłych blokach pamięci.
Osobiście dopiero po wyczerpaniu tych możliwości rozważyłbym coś takiego. Myślę, że to dobry pomysł, jeśli alternatywa jest jak bardziej prywatne metody narażone na nagłówek, a być może tylko ezoteryczny charakter jest praktyczną troską.
Alternatywa
Jedna alternatywa, która właśnie pojawiła się w mojej głowie, która w dużej mierze osiąga te same cele, gdy nieobecni przyjaciele są następująca:
struct PredicateListData
{
int somePrivateField;
};
class PredicateList
{
PredicateListData data;
public:
bool match() const;
};
// In source file:
static bool fullMatch(const PredicateListData& p)
{
// Can access p.somePrivateField here.
}
bool PredicateList::match() const
{
return fullMatch(data);
}
To może wydawać się bardzo sporą różnicą i nadal nazwałbym to „pomocnikiem” (w potencjalnie uwłaczającym sensie, ponieważ wciąż przekazujemy cały wewnętrzny stan klasy do funkcji, czy potrzebuje wszystkiego, czy nie) z wyjątkiem tego, że pozwala uniknąć czynnika „szoku” friend
. Zasadniczo friend
wygląda to trochę przerażająco, gdy często nieobecna jest dalsza inspekcja, ponieważ mówi, że wewnętrzne elementy klasy są dostępne gdzie indziej (co niesie ze sobą sugestię, że może nie być w stanie utrzymać własnych niezmienników). Sposób, w jaki korzystasz friend
, staje się raczej dyskusyjny, jeśli ludzie są świadomi tej praktyki od czasufriend
po prostu znajduje się w tym samym pliku źródłowym, pomagając wdrożyć prywatną funkcjonalność klasy, ale powyższe osiąga ten sam efekt, przynajmniej z jedną możliwą sporną korzyścią, że nie angażuje żadnych przyjaciół, którzy unikają tego rodzaju ( strzelać, ta klasa ma przyjaciela. Skąd jeszcze można uzyskać dostęp do mutacji? ”). Podczas gdy bezpośrednio powyższa wersja natychmiast informuje, że nie ma możliwości uzyskania dostępu / zmutowania szeregów prywatnych przed wszystkim, co zrobiono w trakcie wdrażania PredicateList
.
Być może zmierza to w kierunku nieco dogmatycznych terytoriów z tym poziomem niuansów, ponieważ każdy może szybko dowiedzieć się, czy jednolicie nazywasz rzeczy *Helper*
i umieszczasz je wszystkie w tym samym pliku źródłowym, który jest zgrupowany razem jako część prywatnej implementacji klasy. Ale jeśli zrobimy się wybredni, być może natychmiastowy styl nie spowoduje tak gwałtownej reakcji na kolano na pierwszy rzut oka, bez friend
słowa kluczowego, które wydaje się nieco przerażające.
W przypadku pozostałych pytań:
Konsument może zdefiniować własną klasę PredicateList_HelperFunctions i pozwolić im uzyskać dostęp do pól prywatnych. Chociaż nie uważam tego za ogromny problem (jeśli naprawdę chciałeś na tych prywatnych polach, możesz zrobić casting), może zachęciłoby to konsumentów do korzystania w ten sposób?
Może to być możliwe ponad granicami API, w których klient mógłby zdefiniować drugą klasę o tej samej nazwie i uzyskać w ten sposób dostęp do elementów wewnętrznych bez błędów łączenia. Z drugiej strony jestem w dużej mierze koderem C pracującym w grafice, w którym bezpieczeństwo na tym poziomie „co jeśli” jest bardzo niskie na liście priorytetów, więc takie obawy to tylko te, na które zwykle macham rękami i tańczę i próbuj udawać, że nie istnieją. :-D Jeśli pracujesz w domenie, w której takie obawy są dość poważne, myślę, że warto wziąć to pod uwagę.
Powyższa alternatywna propozycja pozwala również uniknąć tego problemu. Jeśli jednak nadal chcesz używać friend
, możesz również uniknąć tego problemu, zmieniając pomocnika w prywatną klasę zagnieżdżoną.
class PredicateList
{
...
// Declare nested class.
class Helper;
// Make it a friend.
friend class Helper;
public:
...
};
// In source file:
class PredicateList::Helper
{
...
};
Czy to dobrze znany wzorzec projektowy, od którego pochodzi nazwa?
Według mojej wiedzy żaden. Wątpię, by istniał, ponieważ naprawdę zagłębia się w szczegóły szczegółów i stylu implementacji.
„Helper Hell”
Otrzymałem prośbę o dodatkowe wyjaśnienie na temat tego, jak czasami się mylę, gdy widzę implementacje z dużą ilością „pomocniczego” kodu, i może to być nieco kontrowersyjne z niektórymi, ale w rzeczywistości jest to faktyczne, ponieważ tak naprawdę zrobiłem cringe, gdy debugowałem niektóre wdrożenia klasy przez moich kolegów tylko po to, by znaleźć mnóstwo „pomocników”. :-D Nie byłem jedynym w zespole, który drapał się po głowie, próbując dowiedzieć się, co dokładnie powinni zrobić wszyscy ci pomocnicy. Nie chcę też odejść od dogmatyki, takiej jak: „Nie powinieneś używać pomocników”, ale dałbym małą sugestię, że może pomóc pomyśleć o tym, jak wdrożyć rzeczy nieobecne, gdy jest to praktyczne.
Czy wszystkie prywatne funkcje członka nie są funkcjami pomocniczymi z definicji?
I tak, uwzględniam metody prywatne. Jeśli widzę klasę z prostym publicznym interfejsem, ale jak niekończący się zestaw prywatnych metod, które są nieco źle zdefiniowane w celu jak find_impl
lub find_detail
lub find_helper
, to również kulę się w podobny sposób.
Jako alternatywę sugeruję nieprzyjazne funkcje niepowiązane z wewnętrznym łączeniem (zadeklarowane static
lub wewnątrz anonimowej przestrzeni nazw), aby pomóc wdrożyć klasę w co najmniej bardziej ogólnym celu niż „funkcja, która pomaga implementować inne”. I mogę tu przytoczyć Herb Sutter z C ++ „Kodowanie standardów”, dlaczego może to być lepsze z ogólnego punktu widzenia SE:
Unikaj opłat członkowskich: tam, gdzie to możliwe, wolą robić funkcje nieprzyjazne członkom. [...] Nieprzyjazne funkcje nieprzyjazne poprawiają enkapsulację poprzez minimalizowanie zależności: Ciało funkcji nie może zależeć od niepublicznych członków klasy (patrz punkt 11). Rozbijają także klasy monolityczne, aby uwolnić rozdzielną funkcjonalność, co dodatkowo zmniejsza sprzężenie (patrz pozycja 33).
Można również zrozumieć „opłaty członkowskie”, o których mówi w pewnym stopniu, w odniesieniu do podstawowej zasady zawężania zakresu zmiennego. Jeśli wyobrażasz sobie, jako najbardziej skrajny przykład, obiekt Boga, który ma cały kod wymagany do uruchomienia całego programu, faworyzuj tego rodzaju „pomocników” (funkcje, członków lub znajomych), którzy mogą uzyskać dostęp do wszystkich elementów wewnętrznych ( privates) klasy w zasadzie czynią te zmienne nie mniej problematycznymi niż zmienne globalne. Masz wszystkie trudności z prawidłowym zarządzaniem stanem i bezpieczeństwem wątków oraz utrzymywaniem niezmienników, które można uzyskać za pomocą zmiennych globalnych w tym najbardziej ekstremalnym przykładzie. I oczywiście, większość prawdziwych przykładów nie jest tak blisko tego ekstremum, ale ukrywanie informacji jest tylko tak przydatne, ponieważ ogranicza zakres dostępnych informacji.
Teraz Sutter podaje tutaj dobre wyjaśnienie, ale dodam też dalej, że oddzielenie ma tendencję do promowania jak poprawa psychologiczna (przynajmniej jeśli twój mózg działa jak mój) pod względem sposobu projektowania funkcji. Kiedy zaczynasz projektować funkcje, które nie mogą uzyskać dostępu do wszystkiego w klasie, z wyjątkiem tylko odpowiednich parametrów, które przekazujesz lub, jeśli przekazujesz instancję klasy jako parametr, tylko jej publiczne elementy, ma ona tendencję do promowania sposobu myślenia, który faworyzuje funkcje, które mają bardziej przejrzysty cel, oprócz oddzielania i promowania ulepszonego hermetyzacji, niż to, co w innym przypadku można by skusić zaprojektować, gdybyś miał dostęp do wszystkiego.
Jeśli wrócimy do skrajności, baza kodów pełna zmiennych globalnych nie kusi programistów do projektowania funkcji w sposób jasny i uogólniony. Bardzo szybko im więcej informacji można uzyskać w funkcji, tym bardziej wielu z nas śmiertelników staje w obliczu pokusy zdegenerowania jej i zmniejszenia jej przejrzystości na korzyść dostępu do wszystkich tych dodatkowych informacji, które mamy zamiast akceptować bardziej szczegółowe i odpowiednie parametry dla tej funkcji zawęzić dostęp do państwa i rozszerzyć jego zastosowanie oraz poprawić jasność intencji. Dotyczy to (choć ogólnie w nieco mniejszym stopniu) funkcji członków lub znajomych.