Omówienie aplikacji domowej część I

Czas rozpocząć szósty tydzień kursu rytualnie od omówienia zadanej aplikacji domowej. Aplikacji domowej, która miała naprawdę sporo zadań do wykonania – mam nadzieję, że ją wykonałeś.

Jeśli nie to proszę Cię, abyś wrócił do aplikacji domowej z piątego tygodnia kursu.

Interfejsy

Zaczniemy po kolei – czyli od zrefaktorowania interfejsów i ich implementacji.

ProductDao

Na pierwszy rzut idzie ProductDao, który musi wyglądać tak:

public interface ProductDao {
    void saveProduct(Product product) throws IOException;
    void saveProducts(List<Product> products) throws FileNotFoundException;
    void removeProductById(Long productId) throws IOException;
    void removeProductByName(String productName) throws IOException;
    List<Product> getAllProducts() throws IOException;
}

Czyli dużych zmian nie ma – trzeba usunąć metody getProductById oraz getProductByProductName w ProductDaoImpl.

UserDao

public interface UserDao {
    void saveUser(User user) throws IOException;
    void saveUsers(List<User> users) throws FileNotFoundException;
    void removeUserById(Long userId) throws IOException;
    void removeUserByLogin(String login) throws IOException;
    List<User> getAllUsers() throws IOException;
    

Tutaj również do wyrzucenia są metody getUserById oraz getUserByLogin UserDaoImpl – zdecydowałem, że takie metody powinny być bezpośrednio w serwisie.

ProductService

public interface ProductService {
    List<Product> getAllProducts() throws IOException;
    Integer getCountProducts() throws IOException;
    Product getProductByProductName(String productName) throws IOException;
    boolean isProductOnWarehouse(String productName);
    boolean isProductExist(String productName);
    boolean isProductExist(Long productId);
    boolean saveProduct(Product product);
}

Do productService dołącza nowa metoda saveProduct – spójrzmy na jej implementancję.

public boolean saveProduct(Product product) {
    try {
        if (productValidator.isValidate(product)) {
            productDao.saveProduct(product);
            return true;
        }
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }

    return false;
}

Na początku sprawdzamy czy produkt jest poprawny – wykorzystuję już tutaj wcześniej utworzony productValidator, który jest jednym z kolejnych zadań aplikacji domowej w tym tygodniu.

Jeśli wszystko jest spełnione to zwracam true – w przeciwnym razie false.

UserService

public interface UserService {
    boolean addUser(User user);

    void removeUserById(Long userId) throws IOException;

    List<User> getAllUsers() throws IOException;
    User getUserById(Long userId) throws IOException;
    User getUserByLogin(String login) throws IOException;

    boolean isCorrectLoginAndPassword(String login, String password);
}

W UserService została zrefaktorowana metoda addUser na:

public boolean addUser(User user) {
    try {
        if (isLoginAlreadyExist(user.getLogin())) {
            throw new UserLoginAlreadyExistException();
        }

        if (userValidator.isValidate(user)) {
            userDao.saveUser(user);
            return true;
        }
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
    return false;
}

Która na początku sprawdza czy login już istnieje – jeśli tak to rzuca wyjątek, w przeciwnym razie zapisuje Usera i zwraca true.

Doszły również metody getUserById oraz getUserByName z identyczną implementacją jak wygląły one w UserDaoImpl.

Na koniec pojawiła się metoda isCorrectLoginAndPassword sprawdzająca czy login i hasło użytkownika się zgadza:

public boolean isCorrectLoginAndPassword(String login, String password) {
    User foundUser = getUserByLogin(login);

    if (foundUser == null) {
        return false;
    }

    boolean isCorrectLogin = foundUser.getLogin().equals(login);
    boolean isCorrectPass = foundUser.getPassword().equals(password);

    return isCorrectLogin && isCorrectPass;
}

Metoda na początku szuka Usera o danym loginie – jeśli znajdzie to sprawdza zgoność loginu oraz hasła. 😉

ProductType

Ze względu na zmianę sposub odczytu i zapisu produktów obowiązkowo trzeba dodać productType do każdej klasy produktów – może to być jeden znak charakteryzujący typ produktu.

W przypadku klasy Product jest to:

public final static char PRODUCT_TYPE = 'P';

Oraz stworzenie Stringa do zapisu w pliku:

protected String getBasicProductString() {
    return id + PRODUCT_SEPARATOR + productName + PRODUCT_SEPARATOR + price + PRODUCT_SEPARATOR + weight + PRODUCT_SEPARATOR + color + PRODUCT_SEPARATOR + productCount;
}

@Override
public String toString() {
    return PRODUCT_TYPE + PRODUCT_SEPARATOR + getBasicProductString();
}

Metoda getBasicProductString() służy do zbudowania wspólnej części Stringa dla każdego z produktów.

W przypadku klasy Boots:

public final static char PRODUCT_TYPE = 'B';

A tworzenie Stringa wygląda tak:

@Override
public String toString() {
    return PRODUCT_TYPE + PRODUCT_SEPARATOR + getBasicProductString() + PRODUCT_SEPARATOR + size + PRODUCT_SEPARATOR + isNaturalSkin;
}

Czyli wykorzystujemy metodę getBasicProductString z klasy Product, po której dziedziczymy.

W przypadku klasy Cloth:

public final static char PRODUCT_TYPE = 'C';

Oraz tworzenie Stringa:

@Override
public String toString() {
    return PRODUCT_TYPE + PRODUCT_SEPARATOR + getBasicProductString() + PRODUCT_SEPARATOR + size + PRODUCT_SEPARATOR + material;
}

 

ProductType najlepiej gdy jest statyczne – wtedy w obrębie całej aplikacji będzie stworzone po jednym egzemplarzu każdego typu. W przypadku, gdyby nie był statyczny to każdy nowy obiekt typu Product oznaczałby utworzenie pola ProductType! Jest to po prostu marnowanie pamięci maszyny!

ProductParser

Ze względu, że zmieniliśmy sposób zapisywania produktów do typu to w klasie ProductDaoImpl możemy usunąć wszystkie zmienne związane z product type – nie musimy już nigdzie przekazywać jaki jest typ produktu.

Skoro zmienił się sposób zapisywania to i sposób odczytu również się musi zmienić – tym razem typ produktu jest wczytywany na podstawie pierwszego znaku z wczytanej linii z pliku.

final char productType = productStr.charAt(0);

Wiedząc już jaki jest productType to możemy rozpocząc konserwsję Stringa na Product – korzystając z bloku switch

switch (productType) {

    case Product.PRODUCT_TYPE:
        return convertToProduct(productStr);

    case Cloth.PRODUCT_TYPE:
        return convertToCloth(productStr);

    case Boots.PRODUCT_TYPE:
        return convertToBoots(productStr);
}

Słowo kluczowe break nie jest już konieczne w case, ponieważ return zapewnia już całkowite wyjście z metody.

Oczywiście metody convert-* również muszą lekko się zmienić.

Long id = Long.parseLong(productInformations[0]);

W tym przypadku na pierwszym miejscu w tablicy –  czyli index 0 – jest productType, ID jest na miejscu o indeksie 1. Dlatego wszystkie indexy trzeba przesunąć w górę o 1.

Long id = Long.parseLong(productInformations[1]);
String productName = productInformations[2];
Float price = Float.parseFloat(productInformations[3]);
Float weight = Float.parseFloat(productInformations[4]);
String color = productInformations[5];
Integer productCount = Integer.parseInt(productInformations[6]);
Integer size = Integer.parseInt(productInformations[7]);
Boolean isNaturalSkin = Boolean.parseBoolean(productInformations[8]);

I to samo dla innych product converterów.

ProductDaoImpl Singleton

Skoro usunęliśmy już productType z kalsy ProductDaoImpl to można w końcu zrobić z niej Singleton. Nazwa pliku tym razem będzie zapisana w polu statycznym o nazwie fileName.

private static final String fileName = "products.data";

To tu będą zapisywane wszystkie produkty.

A tak wygląda klasyczna implementacja Singletonu:

private static ProductDao instance = null;

private ProductDaoImpl() {
    try {
        FileUtils.createNewFile(fileName);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public static ProductDao getInstance() {
    if (instance == null) {
        instance = new ProductDaoImpl();
    }

    return instance;
}

Pamętaj, aby metoda getInstance była statyczna – inaczej nie będzie możliwości wywołania jej bez utworzenia obiektu – a przecież ta możliwość jest zablokowana przez prywatny konstruktor!

ProductExceptions

Skoro możemy tworzyć już produkty to warto dodać wyjątki, które wykorzystamy podczas walidacji. Musimy stworzyć cztery wyjątki:

  • ProductCountNegativeException,
  • ProductNameEmptyException,
  • ProductPriceNoPositiveException,
  • ProductWeightNoPositiveException.

I mają to być wyjątki checked czyli musza dziedziczyć po klasie Exception!

public class ProductCountNegativeException extends Exception {
    public ProductCountNegativeException() {
    }

    public ProductCountNegativeException(String message) {
        super(message);
    }
}

Tak wygląda jeden z exceptionów – reszta będzie utworzona analogicznie, tylko przez zmianę nazwy klasy. 😉

ProductValidator

Skoro mamy już wyjątki to możemy stworzyć walidator – walidator ma za zadanie sprawdzać podstawowe pola produktów:

  • czy cena jest liczbą większą od zera,
private boolean isPriceNoPositive(Float price) {
    return price <= 0.0f;
}
  • czy liczba produktów nie jest ujemna,
private boolean isCountNegative(Integer productCount) {
    return productCount < 0;
}
  • czy waga jest większa od zera,
private boolean isWeightNoPositive(Float weight) {
    return weight <= 0.0f;
}
  • czy nazwa czasem nie jest pusta
private boolean isNameEmpty(String productName) {
      return productName.length() == 0;
}

I na koniec wszystko zamykamy w jedną metodę isValid wraz z rzucaniem odpowiednich wyjątków:

public boolean isValidate(Product product) throws ProductNameEmptyException, ProductWeightNoPositiveException, ProductCountNegativeException, ProductPriceNoPositiveException {
    if (isPriceNoPositive(product.getPrice())) {
        throw new ProductPriceNoPositiveException("Product price is no positive.");
    }

    if (isCountNegative(product.getProductCount())) {
        throw new ProductCountNegativeException("Product count is less than 0.");
    }

    if (isWeightNoPositive(product.getWeight())) {
        throw new ProductWeightNoPositiveException("Product weight is less or equals 0.");
    }

    if (isNameEmpty(product.getProductName())) {
        throw new ProductNameEmptyException("Product name cannot be empty.");
    }

    return true;
}

Zwróć uwagę, że każdy warunek został przeniesiony do osobnej metody dzięki temu kod jest dużo czytelniejszy! Nazwa metody od razu nazywa sprawdzany warunek!

Dobrze jest również, aby validator był Singletonem – w końcu wystarczy nam w zupełności jego jeden egzemplarz.

private static ProductValidator instance = null;

 private ProductValidator() {

 }

 public static ProductValidator getInstance() {
     if (instance == null) {
         instance = new ProductValidator();
     };

     return instance;
 }

Przebudowa UserValidator

Póki co nasz UserValidator wyglądał tak:

public class UserValidator {

    private final int MIN_LENGTH_PASSWORD = 6;
    private final int MIN_LENGTH_LOGIN = 4;

    private static UserValidator instance = null;
    private UserDao userDao = UserDaoImpl.getInstance();

    private UserValidator() {

    }

    public static UserValidator getInstance() {
        if (instance == null) {
            instance = new UserValidator();
        }

        return instance;
    }

    public boolean isValidate(User user) throws UserLoginAlreadyExistException, UserShortLengthLoginException, UserShortLengthPasswordException {
        if (isPasswordLengthEnough(user.getPassword()))
            throw new UserShortLengthPasswordException("Password is too short.");

        if (isLoginLengthEnough(user.getLogin()))
            throw new UserShortLengthLoginException("Login is too short.");

        if (isLoginAlreadyExist(user.getLogin()))
            throw new UserLoginAlreadyExistException("User with this login already exists.");

        return true;
    }

    private boolean isPasswordLengthEnough(String password) {
        return password.length() >= MIN_LENGTH_PASSWORD;
    }

    private boolean isLoginLengthEnough(String login) {
        return login.length() >= MIN_LENGTH_LOGIN;
    }

    private boolean isLoginAlreadyExist(String login) {
        User user = null;
        try {
            user = userDao.getUserByLogin(login);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (user == null) return false;

        return true;
    }
}

A w nim pole UserService, a tak wyglądała implementacja UserService:

public class UserServiceImpl implements UserService {

    private static UserServiceImpl instance = null;
    private UserDao userDao = UserDaoImpl.getInstance();
    private UserValidator userValidator = UserValidator.getInstance();

    private UserServiceImpl() {
    }

    public static UserServiceImpl getInstance() {
        if (instance == null) {
            instance = new UserServiceImpl();
        }
        return instance;
    }

    public List<User> getAllUsers() throws IOException {
        return userDao.getAllUsers();
    }

    public void addUser(User user) throws IOException, UserShortLengthPasswordException, UserLoginAlreadyExistException, UserShortLengthLoginException {
        if (userValidator.isValidate(user)) {
            userDao.saveUser(user);
        }
    }

    public void removeUserById(Long userId) throws IOException {
        userDao.removeUserById(userId);
    }


}

A tutaj pole UserValidator – i tak w kółko. Przez taki mały błąd powstawał ostatecznie StackOverflowError – czyli przepełnienie stosu pamięci. Oba obiekty na zmianę siebie ładowały przez co brakło pamięci na maszynie wirtualnej.

Dlatego musimy trzymać się jednej zasady – załączamy walidatory do serwisów i nigdy odwrotnie!

W takim razie musieliśmy usunąć metodę isLoginAlreadyExist z UserValidator do UserService oraz usunąć pole UserService w klasie UserValidator, aby pozbyć się wyżej wspomnianego błędu.

Oczywiście tym razem metodę isLoginAlreadyExist wywołujemy jeszcze przed użyciem walidatora w metodzie addUser.

public boolean addUser(User user) {
    try {
        if (isLoginAlreadyExist(user.getLogin())) {
            throw new UserLoginAlreadyExistException();
        }

        if (userValidator.isValidate(user)) {
            userDao.saveUser(user);
            return true;
        }
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
    return false;
}

Koniec I części

To na tyle w pierwszej częsci, która w głównej mierze skłaniała się do refaktoryzacji kodu, w drugiej zajmiemy się dopisywaniem nowej części. 😉

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
3 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Poul
Poul
5 lat temu

Dlaczego w UserValidator odnosisz się do metody getUserByLogin() z klasy UserDaoImpl skoro ta metoda jest w UserServiceImpl? Sam napisałeś, że ta metoda jest do wyrzucenia z UserDaoImpl i zostaje przeniesiona do Service, a tutaj zonk, mamy ja pobrać od Dao? Nie kumam.

Kamil Klimek
Kamil Klimek
5 lat temu
Reply to  Poul

Nie do końca chyba rozumiem – w akapicie “Przebudowa UserValidator” jest pokazana jeszcze stara implementacja validatora oraz serwisu. Na sam koniec akapitu dopiero jest pokazana metoda addUser, która korzysta już z metody isLoginAlreadyExist z UserService. a w UserValidator nie mamy żadnego pola z Dao.

Zerknij jeszcze raz na aktualny kod, może ja coś pominąłem – jeśli tak jest to daj znać, a dojdę co pogmatwałem. 😉

Poul
Poul
5 lat temu
Reply to  Kamil Klimek

Jest ok, po prostu przeniosłem isLoginAlreadyExist z walidatora do serwisu, ale zapomniałem usunąć, mój błąd 😛
Gdyby UserValidator miał swój interfejs, to bym mógł to wcześniej zauważyć;)

3
0
Would love your thoughts, please comment.x