$30 off During Our Annual Pro Sale. View Details »

Hidden mysteries behind big mobile codebases RELOADED

Hidden mysteries behind big mobile codebases RELOADED

You have a really cool and impactful project, but as soon as your codebase gets bigger, and more and more contributors come into play, things can become challenging in regards to aspects like: code consistency, technical debt, refactoring, application architecture and team organization. Let's jump onboard on this journey and walk through different techniques that can help us keep our code sane and healthy for better scalability.

Disclaimer: This talk is going to be focused from a mobile standpoint but most of the practices included can also be applied to any software project under development.

MEMORIES OR TO FOLLOW UP AFTER THE TALK:

**S.O.L.I.D Principles:**
- https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

**Engineering Design Patterns:**
- There are a bunch here: https://www.tutorialspoint.com/design_pattern/design_pattern_overview.htm

**References mentioned:**
- Martin Fowler -> https://martinfowler.com/ (Such a valuable source of information: browse the topics)
- Robert C. Martin

**Books:**
- Clean Code
- Refactoring
- The Pragmatic Programmer
- The Gang of Four

And then any specific programming language ones. I personally do not marry to any programming language, there is no better or worse programming Language, they have all been design with a purpose in mind.

**REMEMBER:**
- "Use the best tool for the right job"
- "Do not do over-engineering"
- "Share and share and let other people participate. "

Fernando Cejas

April 15, 2017
Tweet

More Decks by Fernando Cejas

Other Decks in Technology

Transcript

  1. hidden mysteries behind
    big mobile codebases.
    'reloaded edition'
    @fernando_cejas

    View Slide

  2. Meet @fernando_cejas
    → Curious learner
    → Software engineer
    → Speaker
    → Works at @soundcloud
    → fernandocejas.com

    View Slide

  3. This begins with a story...
    → You are a happy developer
    → You have a lightweight pet project
    → You are the only maintainer

    View Slide

  4. One man Development Process Model.

    View Slide

  5. At some point in time...
    → Project starts to grow...
    → More features are required...
    → You are extremely happy for its success...

    View Slide

  6. View Slide

  7. First problem: Success!
    Android total installs: 161.854.238
    iOS total installs: 122.423.077

    View Slide

  8. Success under the hoods
    → Codebase grows.
    → No tests.
    → Inconsistency across the code.

    View Slide

  9. View Slide

  10. Unsustainable situation. Why?
    → More requirements/features.
    → More contributors.
    → Time to market and dealines.
    → Complexity going up.

    View Slide

  11. Many questions to answer.
    → Can we add a new functionality fast?
    → Is our codebase prepare to scale?
    → Is it hard to maintain?
    → What about technical debt?
    → How to keep it healthy and sane?
    → Is it easy to onboard new people?
    → What about our team organization?

    View Slide

  12. Fact #1
    If your codebase is hard to work with...then change
    it!

    View Slide

  13. Soundcloud
    → From a monolith to a microservices architecture.
    → How we evolved our codebase on the server side.

    View Slide

  14. Soundcloud App Repo DEMO.

    View Slide

  15. What can we do in terms of...
    → Codebase.
    → Team Organization.
    → Working culture.
    → Processes.
    ...to support big mobile code bases?1
    1 Disclaimer: no silver bullets.

    View Slide

  16. Our Codebase and its worst enemies...

    View Slide

  17. Size
    Methods in app-dev-debug.apk: 95586
    Fields in app-dev-debug.apk: 61738
    Lines of code: 137387

    View Slide

  18. Complexity
    private Node delete(Node h, Key key) {
    // assert get(h, key) != null;
    if (key.compareTo(h.key) < 0) {
    if (!isRed(h.left) && !isRed(h.left.left))
    h = moveRedLeft(h);
    h.left = delete(h.left, key);
    }
    else {
    if (isRed(h.left))
    h = rotateRight(h);
    if (key.compareTo(h.key) == 0 && (h.right == null))
    return null;
    if (!isRed(h.right) && !isRed(h.right.left))
    h = moveRedRight(h);
    if (key.compareTo(h.key) == 0) {
    Node x = min(h.right);
    h.key = x.key;
    h.val = x.val;
    // h.val = get(h.right, min(h.right).key);
    // h.key = min(h.right).key;
    h.right = deleteMin(h.right);
    }
    else h.right = delete(h.right, key);
    }
    return balance(h);
    }

    View Slide

  19. Flaky tests
    +---------------------------------------------------------------+------------------------------------------------------+--------------+
    | ClassName | TestName | FailureCount |
    +---------------------------------------------------------------+------------------------------------------------------+--------------+
    | com.soundcloud.android.tests.stations.StationHomePageTest | testOpenStationShouldResume | 7 |
    | com.soundcloud.android.tests.stream.CardEngagementTest | testStreamItemActions | 4 |
    | com.soundcloud.android.tests.stations.RecommendedStationsTest | testOpenSuggestedStationFromDiscovery | 3 |
    | com.soundcloud.android.tests.player.ads.VideoAdsTest | testQuartileEvents | 2 |
    | com.soundcloud.android.tests.player.ads.VideoAdsTest | testTappingVideoTwiceResumesPlayingAd | 2 |
    | com.soundcloud.android.tests.player.ads.AudioAdTest | testQuartileEvents | 2 |
    +---------------------------------------------------------------+------------------------------------------------------+--------------+

    View Slide

  20. Anti-patterns
    public class NotificationImageDownloader extends AsyncTask {
    private static final int READ_TIMEOUT = 10 * 1000;
    private static final int CONNECT_TIMEOUT = 10 * 1000;
    @Override
    protected Bitmap doInBackground(String... params) {
    HttpURLConnection connection = null;
    try {
    connection = (HttpURLConnection) new URL(params[0]).openConnection();
    connection.setConnectTimeout(CONNECT_TIMEOUT);
    connection.setReadTimeout(READ_TIMEOUT);
    return BitmapFactory.decodeStream(connection.getInputStream());
    } catch (IOException e) {
    e.printStackTrace();
    return null;
    } finally {
    if (connection != null) {
    connection.disconnect();
    }
    }
    }
    }

    View Slide

  21. Technical Debt
    public class PublicApi {
    public static final String LINKED_PARTITIONING = "linked_partitioning";
    public static final String TAG = PublicApi.class.getSimpleName();
    public static final int TIMEOUT = 20 * 1000;
    public static final long KEEPALIVE_TIMEOUT = 20 * 1000;
    public static final int MAX_TOTAL_CONNECTIONS = 10;
    private static PublicApi instance;
    @Deprecated
    public PublicApi(Context context) {
    this(context,
    SoundCloudApplication.fromContext(context).getAccountOperations(),
    new ApplicationProperties(context.getResources()), new BuildHelper());
    }
    @Deprecated
    public PublicApi(Context context, AccountOperations accountOperations,
    ApplicationProperties applicationProperties, BuildHelper buildHelper) {
    this(context, buildObjectMapper(), new OAuth(accountOperations),
    accountOperations, applicationProperties,
    UnauthorisedRequestRegistry.getInstance(context), new DeviceHelper(context, buildHelper, context.getResources()));
    }
    public synchronized static PublicApi getInstance(Context context) {
    if (instance == null) {
    instance = new PublicApi(context.getApplicationContext());
    }
    return instance;
    }
    }

    View Slide

  22. How can we battle this enemies and
    conquer a large mobile code base?

    View Slide

  23. Fact #2
    Architecture matters:
    → New requirements require a new architecture.
    → Scalability requires a new architecture.

    View Slide

  24. Pick an architecture and stick to it
    → Onion Layers
    → Clean Architecture
    → Ports and adapters
    → Model View Presenter
    → Custom combination
    → Your own
    → Sacrificial Architecture?

    View Slide

  25. Benefits of a good architecture:
    → Rapid development.
    → Good Scalability.
    → Consistency across the codebase.

    View Slide

  26. View Slide

  27. Architecture Android
    public class MainActivity extends PlayerActivity {
    @Inject PlaySessionController playSessionController;
    @Inject Navigator navigator;
    @Inject FeatureFlags featureFlags;
    @Inject @LightCycle MainTabsPresenter mainPresenter;
    @Inject @LightCycle GcmManager gcmManager;
    @Inject @LightCycle FacebookInvitesController facebookInvitesController;
    public MainActivity() {
    SoundCloudApplication.getObjectGraph().inject(this);
    }
    protected void onCreate(Bundle savedInstanceState) {
    redirectToResolverIfNecessary(getIntent());
    super.onCreate(savedInstanceState);
    if (savedInstanceState == null) {
    playSessionController.reloadQueueAndShowPlayerIfEmpty();
    }
    }
    @Override
    protected void setActivityContentView() {
    mainPresenter.setBaseLayout(this);
    }
    @Override
    protected void onNewIntent(Intent intent) {
    redirectToResolverIfNecessary(intent);
    super.onNewIntent(intent);
    setIntent(intent);
    }
    private void redirectToResolverIfNecessary(Intent intent) {
    final Uri data = intent.getData();
    if (data != null
    && ResolveActivity.accept(data, getResources())
    && !NavigationIntentHelper.resolvesToNavigationItem(data)) {
    redirectFacebookDeeplinkToResolver(data);
    }
    }
    private void redirectFacebookDeeplinkToResolver(Uri data) {
    startActivity(new Intent(this, ResolveActivity.class).setAction(Intent.ACTION_VIEW).setData(data));
    finish();
    }
    }

    View Slide

  28. Architecture Android
    public class StreamFragment extends LightCycleSupportFragment
    implements RefreshableScreen, ScrollContent {
    @Inject @LightCycle StreamPresenter presenter;
    public StreamFragment() {
    setRetainInstance(true);
    SoundCloudApplication.getObjectGraph().inject(this);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(getLayoutResource(), container, false);
    }
    @Override
    public MultiSwipeRefreshLayout getRefreshLayout() {
    return (MultiSwipeRefreshLayout) getView().findViewById(R.id.str_layout);
    }
    @Override
    public View[] getRefreshableViews() {
    return new View[]{presenter.getRecyclerView(), presenter.getEmptyView()};
    }
    @Override
    public void resetScroll() {
    presenter.scrollToTop();
    }
    private int getLayoutResource() {
    return R.layout.recyclerview_with_refresh_and_page_bg;
    }
    }

    View Slide

  29. Fact #3
    Code evolution implies:
    → Constant refactoring.
    → Exploring new technologies.
    → Taking new approaches.

    View Slide

  30. Refactoring
    → Code evolution.
    → Boy scouting.
    → Baby steps.

    View Slide

  31. Code to refactor:
    private void startProcessing(Map map) {
    Processor myProcessor = new Processor();
    for (Entry entry : map.entrySet()) {
    switch(entry.getKey()) {
    case KEY1:
    myProcessor.processStuffAboutKey1(entry.getValue());
    break;
    case KEY2:
    myProcessor.processStuffAboutKey2(entry.getValue());
    break;
    case KEY3:
    myProcessor.processStuffAboutKey3(entry.getValue());
    break;
    case KEY4:
    myProcessor.processStuffAboutKey4(entry.getValue());
    break;
    ...
    ...
    }
    }
    }

    View Slide

  32. Create an abstraction:
    public interface KeyProcessor {
    void processStuff(String data);
    }

    View Slide

  33. Fill a map with implementation:
    Map processors = new HashMap<>();
    processors.add(key1, new Key1Processor());
    ...
    ...
    processors.add(key4, new Key2Processor());

    View Slide

  34. Use the map in the loop:
    for (Entry entry: map.entrySet()) {
    Key key = entry.getKey();
    KeyProcessor keyProcessor = processors.get(key);
    if (keyProcessor == null) {
    throw new IllegalStateException("Unknown processor for key " + key);
    }
    final String value = entry.getValue();
    keyProcessor.processStuff(value);
    }

    View Slide

  35. Fact #4
    Rely on a good test battery that backs you up.

    View Slide

  36. Technical debt
    "Indebted code is any code that is hard to scan."
    "Technical debt is anything that increases the difficulty of
    reading code."
    → Anti-patterns.
    → Legacy code.
    → Abandoned code.
    → Code without tests.

    View Slide

  37. Fact #5
    Do not let technical debt beat you.

    View Slide

  38. Addressing and detecting technical debt:
    → Technical Debt Radar.
    → Static Analysis tools.

    View Slide

  39. Technical
    Debt Radar

    View Slide

  40. Fact #6
    Favor code readability over performance unless the
    last one is critical for your business.

    View Slide

  41. Perfomance
    → First rule: Always measure.
    → Encapsulate complexity.
    → Monitor it.

    View Slide

  42. Fact #7
    Share logic and common functionality accross
    applications.

    View Slide

  43. At SoundCloud
    → Android-kit.
    → Skippy.
    → Lightcycle.
    → Propeller.

    View Slide

  44. Fact #8
    Automate all the things!

    View Slide

  45. At SoundCloud
    → Continuous building.
    → Continuous integration.
    → Continuous deployment.

    View Slide

  46. Lessons learned so far:
    → Wrap third party libraries.
    → Do not overthink too much and iterate.
    → Early optimization is bad.
    → Trial/error does not always work.
    → Divide and conquer.
    → Prevention is better than cure.

    View Slide

  47. Fact #9
    Work as a team.

    View Slide

  48. Team Organization
    → Platform tech lead
    → Core team
    → Feature teams
    → Testing engineering team

    View Slide

  49. View Slide

  50. Working culture
    → Pair programming.
    → Git branching model.
    → Share knowledge with other platforms.
    → Agile and flexible.
    → Collective Sync meeting.

    View Slide

  51. Processes
    → Onboarding new people.
    → Hiring people.
    → Sheriff.
    → Releasing: Release train model + release captains.
    → Alpha for internal use (Dog fooding).
    → Beta community.

    View Slide

  52. Recap #1
    → #1 If your codebase is hard to work with, just
    change it.
    → #2 Architecture matters.
    → #3 Code evolution implies continuous improvement.
    → #4 Rely on a good test battery that backs you up.
    → #5 Do not let Technical Debt beat you.

    View Slide

  53. Recap #2
    → #6 Favor code readability over performance,
    unless it is critical.
    → #7 Share logic and common functionality accross
    applications.
    → #8 Automate all the things.
    → #9 Work as a team.

    View Slide

  54. Conclusion
    → Use S.O.L.I.D
    → Software development is a joyful ride.
    → Make it fun.

    View Slide

  55. Q & A

    View Slide

  56. Thanks!!!
    → @fernando_cejas
    → fernandocejas.com
    → soundcloud.com/jobs

    View Slide