Для того чтобы писать масштабируемый необходимо знание не только паттернов GOF,
но и архитектурных: MVC, MVP, MVVM, MVI. Важно изучить достоинства и недостатки каждого.
Во главу угла также стоят принципы SOLID, далее приведу небольшие примеры:
1. Single Responsibility principle - принцип единственной ответственности. Метод/класс должен выполнять только одну задачу. Например, метод, предназначенный для загрузки данных из сети, не должен заниматься обработкой ошибок.
Псевдокодом напишу:
spoilerpublic 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;
...
}
}
...
});
}
Вместо этого вторую часть нужно выделить в другой метод/класс.
spoilerpublic 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-веток
spoilerpublic class ToolbarManager{
public void showToolbar(int type){
switch(type){
case MAIN:
....
//огромный кусок кода для показа тулбара для главного экрана
....
break;
case PROFILE:
...
//огромный кусок кода для показа тулбара для экрана профиля
...
...
}
}
}
Решение: воспользоваться одним из принципов ООП - полиморфизмом. Теперь, если понадобится добавить
новый экран, нужно будет просто реализовать интерфейс, и это не будет касаться кода других экранов.
spoilerpublic 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-principlesspoilerpublic 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 году, а Илон Маск старательный человек. Мы живем в эпоху электрокаров:
spoilerpublic class ElectricCar implements Car {
public void turnOnEngine() {
throw new AssertionError("А у меня вообще нет двигателя");
}
public void accelerate() {
//ускорение сумасшедшее!
}
}
Выбрасывая машину без двигателя в общую кучу, мы меняем поведение нашей программы.
Это грубое нарушение принципа подстановки Барбары Лисков. Решить данную проблему будет непросто.
Но одним из решений было бы разбиение нашей модели на мелкие интерфейсы, которые учитывают состояние без двигателя
spoilerpublic 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, убрал некоторые детали для простоты:
spoilerpublic 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);
}
И клиентский код:
spoilereditText.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 для тестов.
spoilerpublic class Repository{
private final NetworkManager networkManager;
private final AppDataBase appDataBase;
public Repository(){
this.networkManager = new NetworkManager();
this.AppDataBase = new AppDataBase();
}
}
Поэтому нам следует как-то развязать жесткую связь, выделив интерфейсы.
spoilerpublic 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());
У Мартина Фаулера есть хорошая книга Рефакторинг: Улучшение существующего кода и Р.Мартина Чистый код