Jak rozłożyć konstruktor?

21

Powiedzmy, że mam klasę wroga, a konstruktor wyglądałby mniej więcej tak:

public Enemy(String name, float width, float height, Vector2 position, 
             float speed, int maxHp, int attackDamage, int defense... etc.){}

Wygląda to źle, ponieważ konstruktor ma tak wiele parametrów, ale kiedy tworzę instancję wroga, muszę określić wszystkie te rzeczy. Chcę również tych atrybutów w klasie Enemy, abym mógł iterować ich listę i uzyskać / ustawić te parametry. Myślałem, że może podklasę Enemy do EnemyB, EnemyA, jednocześnie kodując ich maxHp i inne specyficzne atrybuty, ale wtedy straciłbym dostęp do ich zakodowanych atrybutów, gdybym chciał iterować listę EnemyA (składającą się z EnemyA, EnemyB i EnemyC).

Próbuję tylko nauczyć się, jak kodować w sposób czysty. Jeśli to robi różnicę, pracuję w Javie / C ++ / C #. Doceniany jest każdy punkt we właściwym kierunku.

Travis
źródło
5
Nie ma nic złego w tym, że jeden konstruktor wiąże wszystkie atrybuty. W rzeczywistości w niektórych środowiskach trwałości jest to wymagane. Nic nie mówi, że nie możesz mieć wielu konstruktorów, być może z metodą sprawdzania poprawności, która zostanie wywołana po wykonaniu częściowej konstrukcji.
BobDalgleish,
1
Musiałbym zapytać, czy kiedykolwiek zamierzasz konstruować obiekty wroga w kodzie przy użyciu literałów. Jeśli nie, a nie rozumiem, dlaczego tak robisz, buduj konstruktory, które pobierają dane z interfejsu bazy danych lub ciągu serializacji, lub ...
Zan Lynx

Odpowiedzi:

58

Rozwiązaniem jest połączenie parametrów w typy kompozytowe. Szerokość i wysokość są powiązane koncepcyjnie - określają wymiary wroga i zwykle będą potrzebne razem. Można je zastąpić Dimensionstypem, a może także Rectangletypem obejmującym pozycję. Z drugiej strony, może to więcej sensu do grupy positioni speeddo MovementDatarodzaju, szczególnie jeśli przyspieszenie później wchodzi w obraz. Z kontekstu Zakładam maxHp, attackDamage, defenseitp należą również razem w Statsrodzaju. Zmieniony podpis może wyglądać mniej więcej tak:

public Enemy(String name, Dimensions dimensions, MovementData movementData, Stats stats)

Dokładne informacje o tym, gdzie narysować linie, będą zależeć od reszty kodu i od tego, jakie dane są powszechnie używane razem.

Doval
źródło
21
Dodałbym również, że posiadanie tak wielu wartości może wskazywać na naruszenie zasady pojedynczej odpowiedzialności. A grupowanie wartości w określone obiekty jest pierwszym krokiem do rozdzielenia tych obowiązków.
Euforyczny
2
Nie sądzę, że lista wartości jest problemem SRP; większość z nich jest prawdopodobnie przeznaczona dla konstruktorów klasy podstawowej. Każda klasa w hierarchii może mieć jedną odpowiedzialność. Enemyjest tylko klasą atakowaną Player, ale ich wspólna klasa podstawowa Combatantpotrzebuje statystyk walki.
MSalters
@MSalters To niekoniecznie oznacza problem SRP, ale może. Jeśli będzie musiał wykonać wystarczającą liczbę operacji ograniczania liczby, funkcje te mogą znaleźć drogę do klasy Enemy, gdy powinny one być funkcjami statycznymi / wolnymi (jeśli używa Dimensions/ MovementDatajako zwykłych starych kontenerów danych) lub metodami (jeśli zamieni je w dane abstrakcyjne typy / obiekty). Na przykład, jeśli jeszcze nie stworzył Vector2typu, mógł skończyć matematyką wektorową Enemy.
Doval,
24

Możesz rzucić okiem na wzorzec Konstruktora . Z linku (z przykładami wzorca kontra alternatywy):

[] Wzorzec konstruktora jest dobrym wyborem przy projektowaniu klas, których konstruktory lub fabryki statyczne miałyby więcej niż garść parametrów, zwłaszcza jeśli większość z tych parametrów jest opcjonalna. Kod klienta jest znacznie łatwiejszy do odczytania i zapisania w konstruktorach niż w tradycyjnym teleskopowym konstruktorze, a konstruktory są znacznie bezpieczniejsze niż JavaBeans.

Rory Hunter
źródło
4
Pomocny byłby krótki fragment kodu. To świetny wzorzec do budowania skomplikowanych obiektów lub konstrukcji o różnych nakładach. Możesz także specjalizować się z konstruktorami, takimi jak EnemyABuilder, EnemyBBuilder itp., Które zawierają różne wspólne właściwości. Jest to rodzaj drugiej strony wzorca fabrycznego (jak odpowiedziano poniżej), ale moje osobiste preferencje dotyczą Buildera.
Rob
1
Dzięki, zarówno wzór Konstruktora, jak i Wzory fabryczne wyglądają tak, jakby działały dobrze z tym, co próbuję zrobić ogólnie. Myślę, że kombinacja Builder / Factory i sugestii Dovala może być tym, czego szukam. Edycja: Chyba mogę zaznaczyć tylko jedną odpowiedź; Dam to Dovalowi, ponieważ odpowiada na pytanie tematyczne, ale inni są równie pomocni w moim konkretnym problemie. Dziękuję wam wszystkim.
Travis,
Myślę, że warto zauważyć, że jeśli twój język obsługuje typy fantomowe, możesz napisać wzorzec konstruktora, który wymusza wywołanie niektórych / wszystkich funkcji SetX. Pozwala także upewnić się, że zostaną również wywołani tylko raz (w razie potrzeby).
Thomas Eding,
1
@ Mark16 Jak wspomniano w linku, > Wzorzec konstruktora symuluje nazwane parametry opcjonalne znalezione w Adzie i Pythonie. Wspomniałeś, że używasz również C # w pytaniu, a ten język obsługuje nazwane / opcjonalne argumenty (od C # 4.0), więc może to być inna opcja.
Bob
5

Używanie podklas do ustawiania niektórych wartości nie jest pożądane. Tylko podklasę, gdy nowy typ wroga ma inne zachowanie lub nowe atrybuty.

Wzór fabryki jest zwykle używany do abstrakcyjny nad dokładnym klasy używanego, ale może być również wykorzystane w celu zapewnienia szablonów do tworzenia obiektów:

class EnemyFactory {

    // each of these methods is essentially a template for a kind of enemy

    Enemy enemyA(String name, ...) {
        return new Enemy(name, ..., presetValue, ...);
    }

    Enemy enemyB(String name, ...) {
        return new Enemy(name, ..., otherValue, ...);
    }

    Enemy enemyC(String name, ...) {
        return new EnemySubclass(name, ..., otherValue, ...);
    }

    ...
}

EnemyFactory factory = new EnemyFactory();
Enemy a = factory.enemyA("fred", ...);
Enemy b = factory.enemyB("willy", ...);
amon
źródło
0

Zarezerwowałbym podklasę dla klas reprezentujących obiekt, który możesz chcieć samodzielnie wykorzystać, np. Klasę postaci, w której wszystkie postacie, nie tylko wrogowie, mają imię, prędkość, maxHp lub klasę reprezentującą duszki, które są obecne na ekranie o szerokości, wysokość, pozycja.

Nie widzę nic z natury złego w konstruktorze z wieloma parametrami wejściowymi, ale jeśli chcesz go trochę podzielić, możesz mieć jednego konstruktora, który ustawia większość parametrów, i innego (przeciążonego) konstruktora, którego można użyć aby ustawić określone, a inne ustawić na wartości domyślne.

W zależności od wybranego języka niektórzy mogą ustawić wartości domyślne parametrów wejściowych konstruktora, takie jak:

Enemy(float height = 42, float width = 42);
Encaitar
źródło
0

Przykład kodu, który należy dodać do odpowiedzi Rory Hunter (w Javie):

public class Enemy{
   private String name;
   private float width;
   ...

   public static class Builder{
       private Enemy instance;

       public Builder(){
           this.instance = new Enemy();
       }


       public Builder withName(String name){
           instance.name = name;
           return this;
       }

       ...

       public Enemy build(){
           return instance;
       }
   }
}

Teraz możesz tworzyć nowe instancje wroga w następujący sposób:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();
Toon Borgers
źródło
1
Programiści jest wycieczka koncepcyjne pytań i odpowiedzi oczekuje się wyjaśnienia sprawy . Rzucanie zrzutów kodu zamiast objaśnień przypomina kopiowanie kodu z IDE na tablicę: może wyglądać znajomo, a czasem nawet być zrozumiałe, ale wydaje się dziwne ... po prostu dziwne. Tablica nie ma kompilatora
gnat