@ministry_of_silly_walks

Android development: каким должен быть масштабируемый код?

Доброго времени суток, коллеги.
Я начинающий андроид разработчик, и пока не имею опыта в коммерческих проектах.
Мои собственные проекты содержат небольшое количество активностей/экранов.
Подскажите пожалуйста, где можно почитать про "правила хорошего тона" для приложений с 100+ экранами?
Т.е. чем отличается хорошо мастабируемый код от плохо масштабируемого кода в Android?
  • Вопрос задан
  • 272 просмотра
Решения вопроса 1
@terminator-light
Для того чтобы писать масштабируемый необходимо знание не только паттернов GOF,
но и архитектурных: MVC, MVP, MVVM, MVI. Важно изучить достоинства и недостатки каждого.
Во главу угла также стоят принципы SOLID, далее приведу небольшие примеры:
1. Single Responsibility principle - принцип единственной ответственности. Метод/класс должен выполнять только одну задачу. Например, метод, предназначенный для загрузки данных из сети, не должен заниматься обработкой ошибок.
Псевдокодом напишу:
spoiler
public void loadData(String url){
	repository.fetchProducts().get(products->{
		view.showProducts(products);
	}, throwable ->{
		if(throwable instanceof IOexception){
			view.showNoNetwork();
		}else(throwable instanceof HTTPException){
			HTTPException exception = (HTTPException)throwable;
		 	switch(exception.getCode()){
		 		case 400:
		 			view.showError(exception.getMessage());
		 			break;
		 		case 401:
		 			view.showUnauthorized();
		 			break;
		 			...
		 	}
		}
		...
	});
}

Вместо этого вторую часть нужно выделить в другой метод/класс.
spoiler
public void loadData(String url){
	repository.fetchProducts().get(products-> view.showProducts(products), 
		throwable -> ErrorUtil.handleError(throwable, view));
}

public class ErrorUtil{
	public static void handleError(Throwable throwable, View view){
		if(throwable instanceof IOexception){
			view.showNoNetwork();
		}else(throwable instanceof HTTPException){
			HTTPException exception = (HTTPException)throwable;
		 	switch(exception.getCode()){
		 		case 400:
		 			view.showError(exception.getMessage());
		 			break;
		 		case 401:
		 			view.showUnauthorized();
		 			break;
		 			...
		 	}
		}
		...
	}
}


2. Open/Closed principle - принцип открытости/закрытости. Код должен быть открыт для добавления функциональности, но закрыт для изменения.
Например, есть такой код для работы с тулбаром. Если экранов будет много с разными тулбарами,
то постоянно придется добавлять новую ветку case, а значит изменять класс ToolbarManager,
при этом есть возможность появления ошибки в местах, касающихся и других case-веток
spoiler
public class ToolbarManager{
	public void showToolbar(int type){
		switch(type){
			case MAIN:
				....
				//огромный кусок кода для показа тулбара для главного экрана
				....
				break;
			case PROFILE:
				...
				//огромный кусок кода для показа тулбара для экрана профиля
				...
			...
		}
	}
}

Решение: воспользоваться одним из принципов ООП - полиморфизмом. Теперь, если понадобится добавить
новый экран, нужно будет просто реализовать интерфейс, и это не будет касаться кода других экранов.
spoiler
public interface ToolbarManager{
	void showToolbar();
}

public class MainToolbarManager implements ToolbarManager{
	public void showToolbar(){
		....
		//огромный кусок кода для показа тулбара для главного экрана
		....
	}
}

public class ProfileToolbarManager implements ToolbarManager{
	public void showToolbar(){
		....
		//огромный кусок кода для показа тулбара для экрана профиля
		....
	}
}

3. Liskov Substitution principle - принцип подстановки Барбары Лисков гласит: Если класс B - это подтип A, то мы должны иметь
возможность заменить A на B, не нарушая поведение программы.
Для данного принципа не могу придумать пример, связанный с Android, но мне понравился этот пример,
взятый из этого сайта https://www.baeldung.com/solid-principles
spoiler
public interface Car {
    void turnOnEngine(); //запустить двигатель
    void accelerate(); //подать газ
}

public class MotorCar implements Car {
 
    private Engine engine;
 
    public void turnOnEngine() {
        //вруби мотор!
        engine.on();
    }
 
    public void accelerate() {
        //поезжай вперед!
        engine.powerOn(1000);
    }
}

Наш класс удовлетворяет интерфейсу, у нас есть машина, которая имеет свой мотор,
и мы можем ускориться. Но мы живем в 2019 году, а Илон Маск старательный человек. Мы живем в эпоху электрокаров:
spoiler
public class ElectricCar implements Car {
 
    public void turnOnEngine() {
        throw new AssertionError("А у меня вообще нет двигателя");
    }
 
    public void accelerate() {
        //ускорение сумасшедшее!
    }
}

Выбрасывая машину без двигателя в общую кучу, мы меняем поведение нашей программы.
Это грубое нарушение принципа подстановки Барбары Лисков. Решить данную проблему будет непросто.
Но одним из решений было бы разбиение нашей модели на мелкие интерфейсы, которые учитывают состояние без двигателя
spoiler
public interface Engineful {

    void turnOnEngine();
}
public interface Acceleratable {
    void accelerate();
}
public class MotorCar implements Engineful, Acceleratable {
 
    private Engine engine;
 
    public void turnOnEngine() {
        //вруби мотор!
        engine.on();
    }
 
    public void accelerate() {
        //поезжай вперед!
        engine.powerOn(1000);
    }
}
public class ElectricCar implements Acceleratable {
    public void accelerate() {
        //ускорение сумасшедшее!
    }
}


4. Interface segregation principle - прин­цип раз­де­ле­ния интер­фейса.
Создавайте гранённые мелкие интерфейсы. Клиентский код не должен зависеть от функций интерфейса, которые он
не будет использовать. Возьмем пример из Android SDK, убрал некоторые детали для простоты:
spoiler
public interface TextWatcher{
    public void beforeTextChanged(CharSequence s, int start, int count, int after);
    public void onTextChanged(CharSequence s, int start, int before, int count);
    public void afterTextChanged(Editable s);
}


И клиентский код:
spoiler
editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
    	//метод, который нам нужен
    	//какие-то полезные действия
    }
});

Как видно остальные методы нам не нужны, и мы их не используем. Здесь явное нарушение ISP,
т.к. интерфейс навязывает использование других методов

5. Dependency inversion principle - принцип инверсии зависимостей.
Высокоуровневые модули не должны зависеть от низкоуровневых.
Абстракции не должны зависеть от деталей. Но детали зависят от абстракций.
Пример: у нас есть класс Repository, отвечающий за получение данных из разных источников.
Проблема в том, что объекты жестко заданы в конструкторе. И мы не имеем возможности
поменять реализацию AppDataBase на FakeDataBase для тестов.
spoiler
public class Repository{
	private final NetworkManager networkManager;
	private final AppDataBase appDataBase;
	public Repository(){
		this.networkManager = new NetworkManager();
		this.AppDataBase = new AppDataBase();
	}
}


Поэтому нам следует как-то развязать жесткую связь, выделив интерфейсы.
spoiler
public interface RemoteDataSource{}
public interface LocalDataSource{}
public class NetworkManager implements RemoteDataSource{}
public class AppDataBase implements LocalDataSource{}

public class Repository{
	private final RemoteDataSource remoteDataSource;
	private final LocalDataSource localDataSource;
	public Repository(RemoteDataSource remoteDataSource, LocalDataSource localDataSource){
		this.remoteDataSource = remoteDataSource;
		this.localDataSource = localDataSource;
	}
}


А теперь мы можем тестовую реализацию источников:
public class FakeNetworkManager implements RemoteDataSource{}
public class FakeAppDataBase implements LocalDataSource{}

и вызов:
Repository repository = new Repository(new FakeNetworkManager(), new FakeAppDataBase());


У Мартина Фаулера есть хорошая книга Рефакторинг: Улучшение существующего кода и Р.Мартина Чистый код
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
@orbit070
Плохо масштабируемый - это когда у вас в одном классе и девки пляшут и коровы пасутся. Погуглите 'архитектура андроид приложений', mvvm, mvp. Если в двух словах, то разделение приложения на 'слои' и позволяет нормально масштабировать и поддерживать приложение, примеры такого разделения как раз найдете загуглив вышесказанное
Ответ написан
Комментировать
@betterxyz
В любой профессиональной книге по Android программированию.
Например https://www.labirint.ru/books/596118/

Легко масштабируемый продукт — это продукт, на внесение изменений в который уходит минимум средств (под средствами понимаются любые трудозатраты: будь то труд/ часы разработчика или тестировщика, время, затрачиваемое на управление проектами и даже время на коммуникацию!)


Информация
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы