Jak zmienia się koncepcja klasy podczas przekazywania danych do konstruktora zamiast parametrów metody?

12

Powiedzmy, że tworzymy parser. Jednym z wdrożeń może być:

public sealed class Parser1
{
    public string Parse(string text)
    {
       ...
    }
}

Lub możemy zamiast tego przekazać tekst do konstruktora:

public sealed class Parser2
{
    public Parser2(string text)
    {
       this.text = text;
    }

    public string Parse()
    {
       ...
    }
}

Użycie jest proste w obu przypadkach, ale co to znaczy umożliwić wprowadzanie parametrów w Parser1porównaniu do innych? Jaką wiadomość wysłałem do innego programisty, gdy spojrzał na interfejs API? Czy w niektórych przypadkach są jakieś zalety / wady techniczne?

Pojawia się kolejne pytanie, kiedy zdaję sobie sprawę, że interfejs byłby zupełnie bez znaczenia w drugiej implementacji:

public interface IParser
{
    string Parse();
}

... gdzie interfejs na pierwszym może służyć przynajmniej celowi. Czy to w szczególności oznacza, że ​​klasa jest „możliwa do zinterpretowania”, czy nie?

Ciscoheat
źródło
To pierwszy raz, kiedy widziałem kilka przydatnych pytań i odpowiedzi na temat tego, jak semantyka OO faktycznie wyraża zamiar. Do tej pory wszystko to było dla mnie tylko składniowym cukrem.

Odpowiedzi:

11

Semantycznie rzecz biorąc, w OOP powinieneś przekazać konstruktorowi tylko zestaw parametrów, które są potrzebne do zbudowania klasy - podobnie, gdy wywołujesz metodę, powinieneś przekazywać tylko parametry potrzebne do wykonania jej logiki biznesowej.

Parametry, które należy przekazać w konstruktorze, są parametrami, które nie mają sensownej wartości domyślnej, a jeśli twoja klasa jest niezmienna (a nawet a struct), wówczas należy przekazać właściwości inne niż domyślne.

Jeśli chodzi o twój dwa przykład:

  • przekazanie textkonstruktora sugeruje, że Parser2klasa zostanie zbudowana specjalnie w celu późniejszego przeanalizowania tego wystąpienia tekstu. Będzie to konkretny parser. Zwykle dzieje się tak, gdy budowanie klasy jest bardzo drogie lub subtelne, RegEx może zostać skompilowany w konstruktorze, więc gdy już utrzymasz instancję, możesz ponownie jej użyć bez ponoszenia kosztów kompilacji; innym przykładem jest inicjowanie PRNG - lepiej, jeśli robi się to rzadko.
  • jeśli przejdziesz textdo metody, oznacza to, że Parser1może ona zostać ponownie użyta do parsowania różnych tekstów przez wywołania.
Sklivvz
źródło
3
Dodam, że istnieje słaby sygnał, że Parser1 utrzymuje stan - tzn. Że określony ciąg tekstu może dawać różne wyniki, w zależności od tego, co zostało wcześniej zrobione na instancji tat. Niekoniecznie tak jest, ale może tak być.
jmoreno
8

Przypomnijmy, co to znaczy przekazać zmienną jako parametr konstruktora: Inicjujesz obiekt, aby użyć jego zmiennych instancji w metodach obiektu. Chodzi o to, że prawdopodobnie chcesz użyć go w więcej niż jednej metodzie, ponieważ chcesz mieć wysoką spójność w swojej klasie.

Przekazywanie parametru bezpośrednio do metody oznacza wysłanie wiadomości do obiektu i prawdopodobnie otrzymanie odpowiedzi. W ten sposób klient chce, aby obiekt świadczył dla niego usługę.

Podsumowując, są to dwa bardzo różne sposoby przekazywania parametrów i powinieneś wybrać, czy Twój obiekt ma dostarczać usługę, czy też zapewniać funkcjonalność podczas zarządzania wewnętrznymi informacjami.

McMannus
źródło
+1. Spójność to sposób, w jaki decyduję, czy należy ona do metody, czy konstruktora.
jhewlett
4

Użycie jest proste w obu przypadkach, ale co to znaczy włączyć wprowadzanie parametrów do Parser1, w porównaniu do drugiego?

Jest to fundamentalna zmiana projektu. A projekt powinien przekazywać intencje i znaczenie. Czy potrzebujesz osobnych obiektów dla każdego łańcucha, który chcesz przeanalizować? Innymi słowy, dlaczego potrzebujemy wystąpienia parsera z stringX i innego wystąpienia z stringY? Co takiego jest w analizie składni i danym łańcuchu, że oboje muszą żyć i umrzeć razem? Zakładając, że „podstawowa implementacja [parsowania]” (jak mówi Robert Harvey) nie zmienia się, wydaje się, że nie ma sensu. I nawet wtedy jego wątpliwe IMHO.

Jak zmienia się koncepcja klasy podczas przekazywania danych do konstruktora zamiast parametrów metody?

Parametry konstruktora mówią mi, że te rzeczy są wymagane dla obiektu. Bez nich nie można zagwarantować właściwego stanu. Wiem także, dlaczego / dlaczego jeden parser zasadniczo różni się od drugiego.

Parametry konstruktora uniemożliwiają mi zbyt dużą wiedzę na temat korzystania z klasy. Jeśli zamiast tego mam ustawić określone właściwości - skąd mam to wiedzieć? Otwiera się cała puszka robaków. Jakie właściwości W jakiej kolejności? Przed użyciem jakich metod? i tak dalej.

Pojawia się kolejne pytanie, kiedy zdaję sobie sprawę, że interfejs byłby zupełnie bez znaczenia w drugiej implementacji:

Interfejs, podobnie jak w API, to metody i właściwości narażone na kod klienta. Nie daj się owinąć public interface { ... }wyłącznie. Zatem znaczenie interfejsu wynika z dylematu parametru konstruktor vs konstruktor vs metoda, NIE public interface Iparservspublic sealed class Parser

sealedKlasa jest nieparzysta. Jeśli myślę o różnych implementacjach parsera - wspomniałeś o „Iparserze” - to moją pierwszą myślą jest dziedziczenie. To tylko naturalne przedłużenie mojego myślenia. IE wszystkie ParserXsą zasadniczo Parsers. Jak inaczej to powiedzieć? ... Owczarek niemiecki jest psem (dziedziczenie), ale mogę wyszkolić papugę do szczekania (zachowywać się jak pies - „interfejs”); ale Polly nie jest psem, udaje tylko, że nauczył się podzbioru psości. Klasy, abstrakcyjne lub inne, doskonale służą jako interfejsy .

radarbob
źródło
Jeśli chodzi jak parser i mówi jak parser, to jest ... kaczką!
2

Druga wersja klasy może być niezmienna.

Interfejs może być nadal używany, aby zapewnić możliwość wymiany podstawowej implementacji.

Robert Harvey
źródło
Czy nie można tego uczynić niezmiennym również w pierwszej wersji, przekazując dane wewnątrz klasy w sposób funkcjonalny?
ciscoheat
2
Absolutnie. Ale ogólnym wzorcem niezmienności jest ustawienie elementów klasy za pomocą konstruktora i posiadanie właściwości tylko do odczytu. Do programowania funkcjonalnego nie potrzebujesz nawet klasy.
Robert Harvey
Scylla i Charybdis: czy wybieram OO czy niezmienne dane? Nigdy wcześniej tego nie słyszałem.
2

Parser1

Budowanie przy użyciu domyślnego konstruktora i przekazywanie tekstu wejściowego do metody oznacza, że ​​Parser1 może być ponownie użyty.

Parser2

Przekazywanie tekstu wejściowego do konstruktora oznacza, że ​​dla każdego łańcucha wejściowego należy utworzyć nowy Parser2.

Mike Partridge
źródło
Jasne, więc jeśli przejdziemy na wyższy poziom, co wyciągnąć wniosek na temat przedmiotu wielokrotnego użytku w porównaniu do przedmiotu wielokrotnego użytku?
ciscoheat
Nie zakładałbym nic więcej, zamiast tego odkładać na dokumentację.
Mike Partridge
„Obiekt wielokrotnego użytku”, w którym zmienia się jego tożsamość, nie jest sekwencją. A dzięki zarządzanym ramom, co oznacza, że ​​nie musisz nawet wyrzucać rzeczy, po prostu nadpisywać je lub wykraczać poza zakres, jest to z pewnością niepotrzebne. Zbuduj!