Wiele osób już odpowiedziało. Myślałem, że dam własną perspektywę.
Dawno, dawno temu pracowałem nad aplikacją (i nadal tak robię), która tworzy muzykę.
Aplikacja miała abstrakcyjną Scale
klasę z kilku podklas: CMajor
, DMinor
itp Scale
wyglądał mniej więcej tak więc:
public abstract class Scale {
protected Note[] notes;
public Scale() {
loadNotes();
}
// .. some other stuff ommited
protected abstract void loadNotes(); /* subclasses put notes in the array
in this method. */
}
Generatory muzyki współpracowały z konkretną Scale
instancją w celu generowania muzyki. Użytkownik wybiera skalę z listy, z której ma być generowana muzyka.
Pewnego dnia przyszedł mi do głowy fajny pomysł: dlaczego nie pozwolić użytkownikowi na tworzenie własnych wag? Użytkownik wybiera notatki z listy, naciska przycisk, a nowa skala zostanie dodana do listy dostępnych skal.
Ale nie byłem w stanie tego zrobić. Stało się tak, ponieważ wszystkie skale są już ustawione w czasie kompilacji - ponieważ są wyrażone jako klasy. Potem uderzyło mnie to:
Często intuicyjne jest myślenie w kategoriach „nadklas i podklas”. Prawie wszystko można wyrazić za pomocą tego systemu: nadklasa Person
i podklasy John
oraz Mary
; nadklasa Car
i podklasy Volvo
oraz Mazda
; nadklasa Missile
i podklasy SpeedRocked
, LandMine
oraz TrippleExplodingThingy
.
Myślenie w ten sposób jest bardzo naturalne, szczególnie dla osoby stosunkowo nowej w OO.
Ale zawsze powinniśmy pamiętać, że klasy to szablony , a obiekty to zawartość wlewana do tych szablonów . Możesz wlać dowolną zawartość do szablonu, tworząc niezliczone możliwości.
Wypełnienie szablonu nie jest zadaniem podklasy. To zadanie obiektu. Zadaniem podklasy jest dodanie faktycznej funkcjonalności lub rozwinięcie szablonu .
I dlatego powinienem był stworzyć konkretną Scale
klasę z Note[]
polem i pozwolić obiektom wypełnić ten szablon ; ewentualnie przez konstruktora lub coś takiego. I w końcu tak zrobiłem.
Za każdym razem, gdy projektujesz szablon w klasie (np. Pusty Note[]
element, który należy wypełnić lub String name
pole, któremu należy przypisać wartość), pamiętaj, że wypełnianie szablonu jest zadaniem obiektów tej klasy ( lub ewentualnie osoby tworzące te obiekty). Podklasy mają na celu zwiększenie funkcjonalności, a nie wypełnianie szablonów.
Możesz mieć ochotę stworzyć „nadklasę Person
, podklasy John
i Mary
” system, tak jak to zrobiłeś, ponieważ podoba ci się formalność, jaką to daje.
W ten sposób możesz po prostu powiedzieć Person p = new Mary()
zamiast Person p = new Person("Mary", 57, Sex.FEMALE)
. Sprawia, że rzeczy są bardziej zorganizowane i bardziej uporządkowane. Ale, jak powiedzieliśmy, tworzenie nowej klasy dla każdej kombinacji danych nie jest dobrym podejściem, ponieważ przesadza z kodem za darmo i ogranicza cię pod względem możliwości działania.
Oto rozwiązanie: użyj podstawowej fabryki, może nawet być statyczna. Tak jak:
public final class PersonFactory {
private PersonFactory() { }
public static Person createJohn(){
return new Person("John", 40, Sex.MALE);
}
public static Person createMary(){
return new Person("Mary", 57, Sex.FEMALE);
}
// ...
}
W ten sposób możesz łatwo korzystać z „ustawień wstępnych” „dostarczanych z programem”, takich jak:, Person mary = PersonFactory.createMary()
ale zastrzegasz sobie również prawo do dynamicznego projektowania nowych osób, na przykład w przypadku, gdy chcesz pozwolić użytkownikowi to zrobić . Na przykład:
// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);
Lub jeszcze lepiej: zrób coś takiego:
public final class PersonFactory {
private PersonFactory() { }
private static Map<String, Person> persons = new HashMap<>();
private static Map<String, PersonData> personBlueprints = new HashMap<>();
public static void addPerson(Person person){
persons.put(person.getName(), person);
}
public static Person getPerson(String name){
return persons.get(name);
}
public static Person createPerson(String blueprintName){
PersonData data = personBlueprints.get(blueprintName);
return new Person(data.name, data.age, data.sex);
}
// .. or, alternative to the last method
public static Person createPerson(String personName){
Person blueprint = persons.get(personName);
return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
}
}
public class PersonData {
public String name;
public int age;
public Sex sex;
public PersonData(String name, int age, Sex sex){
this.name = name;
this.age = age;
this.sex = sex;
}
}
Poniosło mnie. Myślę, że masz pomysł.
Podklasy nie mają na celu wypełnienia szablonów ustawionych przez ich nadklasy. Podklasy mają na celu dodanie funkcjonalności . Obiekty mają wypełniać szablony, po to są.
Nie powinieneś tworzyć nowej klasy dla każdej możliwej kombinacji danych. (Tak jak nie powinienem był tworzyć nowej Scale
podklasy dla każdej możliwej kombinacji Note
s).
Jest wytyczną: za każdym razem, gdy tworzysz nową podklasę, zastanów się, czy dodaje ona nową funkcjonalność, która nie istnieje w tej podklasie. Jeśli odpowiedź na to pytanie brzmi „nie”, być może próbujesz „wypełnić szablon” nadklasy, w takim przypadku po prostu stwórz obiekt. (I ewentualnie Fabryka z „ustawieniami wstępnymi”, aby ułatwić życie).
Mam nadzieję, że to pomaga.