SOLID - Open-Closed Principle
Hm, zwykle gdy chcemy dodać jakąś funkcjonalność po prostu dopisujemy odpowiedni kod. Weźmy przykład:
public class User {
private String login;
private String password;
private UserRepository repo;
public void delete () {
repo.delete(this);
}
public User findByName(String name) {
return repo.findByName(name);
}
}
public class UserRepository {
private EntityManager em;
public void delete(User user) {
em.remove(user);
}
public User findByName(String name) {
return (User) em.createQuery("from User u where u.name=:name")
.setParameter("name", name)
.getSingleResult();
}
}
Klasy proste, śliczne (oczywiście troszkę uproszczone :)). I nagle przychodzi wymaganie: użytkownik który ma uprawnienia administratora ma mieć flagę deleted która oznacza, że tak naprawdę nie ma być usuwany, tylko zaznaczony jako usunięty i nie prezentowany już potem w GUI. No to pierwsze podejście pewnie będzie takie:
public class User {
private String login;
private String password;
private boolean admin;
private boolean deleted;
private UserRepository repo;
public void delete () {
repo.delete(this);
}
public User findByName(String name) {
return repo.findByName(name);
}
}
public class UserRepository {
private EntityManager em;
public User findByName(String name) {
return (User) em.createQuery("from User u where u.name=:name and deleted=false")
.setParameter("name", name)
.getSingleResult();
}
public void delete(User user) {
if (user.isAdmin()) {
user.setDeleted(true);
em.persist(user);
} else {
em.remove(user);
}
}
}
No, gotowe! Tylko, czy to jest dobry design? Już if w metodzie usuwającej wskazuje, że dzieje się tam więcej niż jedna rzecz. Co więcej, zasada pojedynczej odpowiedzialności dużo nam tu nie pomoże, możemy co jakwyżej osiągnąć coś takiego:
public class User {
private String login;
private String password;
private boolean admin;
private boolean deleted;
private UserRepository repo;
public void delete () {
if (! admin)
repo.deleteNonAdmin(this);
else {
repo.deleteAdmin(this);
}
}
public User findByName(String name) {
return repo.findByName(name);
}
}
public class UserRepository {
private EntityManager em;
public User findByName(String name) {
return (User) em.createQuery("from User u where u.name=:name and deleted=false")
.setParameter("name", name)
.getSingleResult();
}
public void deleteNonAdmin(User user) {
em.remove(user);
}
public void deleteAdmin(User user) {
user.setDeleted(true);
em.persist(user);
}
}
Zwiększyliśmy trochę ilość kodu (twój manager już jest zadowolony - kolejna wskaźnik postępu zwiększony!), może trochę lepiej widać na czym polega różnica między usuwaniem administratora i nie-administratora, ale tak naprawdę kod wiele się nie poprawił. Jeśli będą dochodziły kolejne wymagania jak to, nasza metoda delete zmieni się wkrótce w wielki ciąg if-else. I tu właśnie jest miejsce do zastosowania zasady otwartości-zamknięcia.
Co to znaczy w praktyce?
Dziedziczenie.
Jeśli przypomnimy sobie, że w językach obiektowych możemy dziedziczyć, okaże się, że ten kod jest jeszcze do uratowania :)
public class User {
protected String login;
protected String password;
protected UserRepository repo;
public User findByName(String name) {
return repo.findByName(name);
}
public void delete() {
repo.delete(this);
}
}
public class AdminUser extends User {
boolean admin;
private boolean deleted;
}
public class UserRepository {
protected EntityManager em;
public User findByName(String name) {
return (User) em.createQuery("from User u where u.name=:name")
.setParameter("name", name).getSingleResult();
}
public void delete(User user) {
em.remove(user);
}
}
public class AdminUserRepository extends UserRepository {
public User findByName(String name) {
return (User) em.createQuery("from User u where u.name=:name and deleted=false")
.setParameter("name", name)
.getSingleResult();
}
public void delete(AdminUser user) {
user.setDeleted(true);
em.persist(user);
}
}
Teraz if-else w ogóle nie istnieje. Kod wołający metody findByName i delete dalej nie musi wiedzieć, że dana instancja klasy User jest administratorem, ale nowa struktura kodu wyeliminowała potrzebę tworzenia warunków (zwykle było by ich więcej niż tylko w jednym miejscu, więc jeśli w kodzie masz gdzieś ten sam warunek sprawdzany parę razy w różnych metodach, to prawdopodobnie powinieneś zastanowić się nad dziedziczeniem). Kod jest teraz dużo bardziej przejrzysty (np. od razu wiadomo gdzie szukać jeśli coś jest nie tak z administratorem).
Ta zasada wydaje się zupełnie oczywista. Nie trzeba doktoratu z informatyki, żeby wpaść na takie rozwiązanie. Jednak jakże często dodajemy funkcjonalności do kodu bez większego zastanowienia. Po prostu dodajemy kod. Pilnowanie zasady otwartości-zamknięcia jest kolejnym sposobem na nie zwiększanie długu technicznego (nie pogorszanie jakości kodu) wraz z dodawaniem funkcjonalności.
Pisanie i projektowanie aplikacji poprzez stosowanie TDD powoduje, że czas na zastanowienie się nad najlepszym rozwiązaniem staje się nieodłączną częścią procesu rozwoju oprogramowania wykonywaną z resztą co parę minut, co oczywiście ma bezpośredni wpływ na jakość kodu który po sobie pozostawiamy. Istnienie testów powoduje, że nie mamy obaw związanych z wprowadzaniem większych zmian do oprogramowania - np. tak jak w powyższym przykładzie z wymianą warunków na dziedziczenie.
Tak, chcę doprowadzić do obniżenia jakości oprogramowania. Bardziej niż na jakości zależy mi na terminach. Jestem skłonny raczej w przyszłości mieć ciągłe problemy z ukańczaniem funkcjonalności i błędami niż zmienić termin tego releasu.