Darius Sabaliauskas

Managing Complex Screens with Plug-in Architecture

Transcript

Darius

Hi, everyone. My name is Darius, and today I will talk about plugin architecture, and in iOS context it might sound a bit strange, but I assure you that in the end it all makes sense. So I will talk a bit about history, then what is the plugin architecture, and why you might consider using it, and then I will talk a bit about open-close principle and show some examples in UIKit, and by the way, how many of you still using UIKit as primary source for building the UI?

Yeah, still quite some. So about history. So plugin architecture is also known as micro-kernel architecture, and when you hear word micro-kernel, you might think like Unix, Linux, something around those lines, and actually, Unix was kind of in the early beginning, the first attempts to kind of extract things into components, so they have small modular utilities that you can pipe together, and later on, there was two decades of component-based software engineering to attempt to kind of make different components and composition things together, and then in 1997, Internet Explorer was the first major browser that had extensions, that additional functionality, and in 2001, Eclipse was the ED that was from ground up built around plugin architecture, because Eclipse as a baseline is just like text editor, and all the functionality is added to your plugins. So why you might consider using plugin architecture?

So in app, we sometimes have like screens that are pretty complex, so those like product pages, checkout, homepage, that has lots of things going on in one place, and usually what we're trying to do, we're just trying to kind of use those MVVM, VAP, or whatever other patterns to kind of split between presentation logic and business logic, and it was pretty small functionality, it's usually enough, but when it goes larger, the business logic part also grows larger, and it might be not enough, and you might think about other ways how to split it up into smaller pieces, and there, plugin architecture comes into play. So it's pretty important to kind of remind ourselves about open-close principle, and especially in plugin architecture, it kind of means that you have those plugins, the new functionality or additional one that you want to kind of add, and by adding it, it means that you don't change anything that's out there, so it's kind of good in the sense that you kind of don't need to test things that was there before, because you don't change anything. Of course, you should test. But it's the main principle is that you just add new things without changing what is there.

So going to plugin architecture itself, so you have two main parts in plugin architecture, you have that core system, and then you have plugins that integrate in it, and it has some benefits, so for example, it makes it easy to add new functionality to your plugins, and because plugins kind of function out in the box, you can develop it and test it separately, but also, it has some trade-offs, and for example, that core system, you need to design up front, because it's kind of integration point when all those plugins kind of integrate, so if you want to kind of change it later on, it might be pretty expensive, because then you kind of break the contracts, how you kind of manage those plugins and so on, and also, those cons implies that the core system should be pretty small, so in that Eclipse example I mentioned before, you should aim to make that core system as small as possible, because all those changes that you want to make there later on will kind of hit you hard, and in real life, it's usually a bit more complicated than just like simple drawing, just plugins, assemble it and so on, so it's more things going on there, so here's an example of demo checkout screen, which is somewhat complex, so you have list of items, you have total price, you have a field for promo code, you have a button that choosing payment method, and you have pay, so basically you have like these five pieces that kind of do separate things, and as kind of first step, what you're usually trying to do is to kind of disassemble it, to create like using some pattern, separate like presentation logic and business logic, we have some AVM, VAP or any other pattern, and you kind of split it to different views for different sections, and you have corresponding view model or like interact or like what other unit kind of deals with the logic, and then it might not be enough, and you might think about kind of stepping it up a bit, and not only kind of separating the views a bit, but also bonding that logic together with the view, so basically you might consider having like childhood controllers, because then you have like presentation and business logic in the same place, and then you can use like in this case for like MVVM things to compose it together, and what childhood controls also does that you have access to navigation stack, so basically you can also navigate deeper, so for example you have those items, you can click on those, and from that items view controller, you can kind of show the item patient and so on, so you have also the navigation kind of embedded, and you can also step a bit more and have plugins that wraps those view controllers up, and what it does is basically makes in that open-close principle that in that previous example of two controllers, so not necessarily not changing the code when you're adding new controller, you probably to add new view controller, you need to have some enum of the list, what is there, some other things, so we're not changing, not creating like new view controller, just like plugs in, you're just changing different places around your code base to kind of achieve that, and with plugins to kind of bundle that, and the main idea is that you don't need to change the other stuff that was there before, so I'll explain a bit how we can achieve that, so here's the example how that kind of breakdown could look, so we have like for example, Chicago controller, we have some kind of service that does the get response to get the content of that screen, and I have the post on pawn for doing the payment, and you click the pay button, and then you have that car system that basically coordinates the plugin logic, so each element create a plugin, and you need like a banner, just create a banner plugin, just put it in, and then it works, so ideally it won't change anything, but you need to change something, so in this case, you have some kind of registry to make awareness of existence of those plugins, with those engines, and basically what this registry does, it makes like single place that you need to change in order to kind of add the plugin, so you if you want a banner, just add new entry with identifier like banner, and banner plugin factory, and then basically in ideal case, it should be only place that you need to change, and then for additional like banner or so, we will just create new code, and you won't touch at all the old code, so basically here are identifiers in the factories that builds it, so it kind of sounds simple, and almost too good to be true, like you don't change anything that was before, just add entry and registry, just magically appears, so there is a catch to it, so the first problem that you probably will face is that response itself, because you want like for example add a banner in checkout or whatever, and you might find out that you not only need to registry, but you need also to update the response to codeable struct to have the data of the banner, so you kind of need to change two places, and you will not necessarily want that, so one way to kind of solve it is just to split the data out of that response to separate requests, so then you are kind of delegating, getting the data to the plugins itself, they do their own request, and so on, and it might be good in some cases, but not all, we modelists usually deal with like single response, and then we need to figure out how to kind of break it down, so another way to kind of do it is to do some custom decoding, so it is a bit sad that there is no proper way to do it with the codeable, say like I want some portion of the response to be kind of decodable or some like as a raw, so there is not very straightforward way to do it, so what usually you need to do is just go the old-fashioned way with JSON serialization, so we have like response data, we decode it to JSON dictionary, then you basically pick the things out of it that belongs to separate kind of plugin or component, and then you basically turn it back to the data, and then you have like array of like identifier like items, and then the data that basically behind the scenes is that JSON of the items list, and that way you kind of repackaging it in more like primitive types, and that way you kind of have some benefits that for example if backend data changes for some like a banner which is not for example relevant, and there was required fields and right now did not return, in usual case your entire response will just throw and you show nothing, but when you kind of repackaging it and delegating the responsibility deeper to the plugin itself, you basically can deal with those kind of error tolerance that you can don't matter that much if some data kind of changes or breaks things, because then individual plugin can deal with that later on, so in that registry there was like identifying that plugin factory, so what that plugin factory kind of does, and I was talking right now about that second example, when you have like disassembled the response or package it, so that core system just checks like there is this kind of entry items in the registry with this factory, and I have this items with data in the response, so I can do some detail object or just linear array, it maps it and then calls the factory with the data to kind of do the magic, and behind the scenes it's nothing magical, this is the example of like JSON, so we have like plugins already in the JSON, we have some identifier like full and then you have the content, so basically when you're repackaging it you get that identifier full and that content JSON just like converted back to the data, and then plugin itself kind of decodes it, it can throw, but then you make a decision in core system, do I want to kind of fail if any factories fail or I say like if some factories fail I don't care, I still draw those that kind of succeed, so in that case like if you messed up with coupon code data or something you can still show the rest and so on, so you have a bit better like error tolerance, and let's back to this slide a bit, so basically you just have checkout view controller, you call that service, you get that content JSON, then you basically do abracadabra and repackaging it, that is basically all close to those primitive types and then you don't depend on it, so basically you add just new items that JSON response, but at the point of checkout view controller you don't know what is there and you don't need to explicitly decode it, you just delegate it to plugins, so just add new entry of the new plugin in the registry and the factories kind of do the decoding for its own part, so it sounds like solved, yes, and it kind of is for some cases, so for example if you have product page, then those kind of pieces in the product page don't really do much communication back, it's basically just kind of displaying things and then if there is some section of it that is failed to decode, it's kind of drawing it and for those cases like product pages, home pages and so on, it's kind of fine, it probably is enough, but the problem arises when you need to have a communication between those different elements, like in checkout example or some other that either core system needs to get something out there, like do some kind of validation or those elements itself needs to kind of communicate between each other, so this is the code snippet of that core system for checkout, so we have layout which basically just returns those new controllers that kind of embedded in the individual plugin and we have like two things that needs communication, so one is refresh, so coupon code plugin in this case says that you enter coupon code and you kind of need to calculate on the back end the pricing, so kind of need to communicate up that, oh you need to kind of refresh everything and other thing is kind of when you click pay, you need to kind of validate and communicate that between those elements, so let's start with validation, so with validation we have like validatable protocol that has this valid function that throws the output and our case we need kind of two things to output out from the plugins, so it's total amount, you know how much you pay and then payment method how you're going to pay and you should remember to use like interface segregation principle and not just put the validation on all of those plugins because most of them don't need it, so you can put it only for total price plugin and payment method plugin because only these kind of care that validation happens and then basically our system will just like filter it out, only plugins that comply to that protocol and then you call the validate on those, if they're throwing error because the plugin itself kind of throws, it can, because it's also drawing its own kind of view in view controller, so it can also show that there's an error and so on and throwing if some data is missing like payment method is not selected and then so and then handle its own UI and then you kind of propagate error back to kind of check out screen and then it can do nothing because individual kind of plugin deals with it or it can additionally show everything like oh you need to fill some data and then individual plugin shows what data is kind of missing. Yes, so let's talk about communication which is kind of more complicated thing when you need some communication between those elements, so we kind of can do it like in similar fashion to create event publishing protocol and event consuming protocol then attach those to those plugins that actually need to publish something or need to kind of consume something, so in our case it's only coupon code that actually publishes that refresh because when you enter in coupon code you need to kind of send it to the backend, recalculate, validate it and so on and you can do in plugin itself the request to say like if the coupon is valid but still price calculation happens for entire checkout and so on and then you should remember in this case that the number of events will be a problem so the smaller amount of those you have the better and in that original screenshot that I showed it was like pay button in the bottom was not kind of changed to plugin and because if you do pay as plugin you then have way more communication because each plugin need to say oh I know there's price change there is the payment mode selected and instead of just like single refresh you have all kinds of communication happening between those elements so it's also kind of decision when you're designing that core system how those elements will be interacting with each other and what you kind of need and if you kind designed wrongly it might be pretty painful to kind of deal with it later on so in this case like keeping out the pay logic outside of plugins make it simpler because then all only thing that during the validation plugin needs to just communicate it out and then payment is done like outside that core system so in that core system you can do simple binding between those plugins so basically do the similar validation we just filter out those plugins that publishing setting out the closure that handles that publish event and then handling it basically notifying each of the plugins that consumes it and in our case it's nothing that consumes that refresh it's only a core system itself that kind of listens to it and just puts out but this logic kind of makes it possible for other kind of type of communication if it's kind of needed in very kind of primitive example though you need to be a bit more careful not to create the cycle here because it can easily fall into that and to summarize it so plugin architecture can modularize complex screens and allow to make the changes and add new functionality without changing any other code that was before there though you need to think quite extensively about the core system and what it does and how it will behave and what kind of future changes you might need to implement because it might backfire so we need to make it small in this example it was like I think only 100 lines of code and you see like majority of it so it was basically call like calling the validation plugins and doing that bind so it was not doing much of it it's just like a glue that put those plugins together and you need to kind of consider trade-offs but maybe just child controllers it's enough for those like separate vp cycles or mvvm and so on and communication is the pain and if you have simple communication I guess it's fine but you have if you have more things going on so probably you need to kind of reconsider how you do that those things and you should remember those kind of solid principles especially open close one because ideally we want our system to to be kind of not changing when we have things and more even outside of like plugins context we can achieve that the better and also with interface segregation that we don't want to kind of just put a lot like lots of things in single protocols that we have just empty implementation for things so you hear about like plugin architecture is one of the ways to kind of modularize complex screens it's not only the way and you might not need it but it can help you also so thank you for you attention.

Audrey

Thank you for your presentation my first question will be about the routing how do you fit the routing in all of this plugin architecture it will will be a plugin too

Darius

it's hard to say like it's usually just enough like if you have child controller because you have the access to kind of navigation stack you just do it from there because overall like navigation and routing between like different things is not solved yet so like coordinator slow controls like you name it routers that people try to kind of solve it and kind of not yet solve problem so it depends on the project needs and in most of the cases

Zino

there's there's a lot of people in in the audience who say why don't you use SwiftUI why do you have to use UIView controller why not UIView it feels to me that your presentation was try not to think about it that way it's not about the views themselves about how you architecture your application and then you can even maybe mix and match if you need to

Darius

yeah you know it depends on the context like SwiftUI is kind of great but when you have like huge code base you don't want to kind of go and just refactor things to SwiftUI just because you can because you have other problems that you need to solve and in a minute we also have like project that's running like for many years there are lots of things and especially in the bigger the project is you don't want to have like here's like one way here's another way and then like mix of different things but definitely like at some point we will be switching and also like you need to kind of support older iOS versions so because SwiftUI in like early stages was not that great and like have some pitfalls all right thanks and Darius what would you say to people

Audrey

who like to modularize their application like trying to build Swift packages and trying to respect the open close principle what would you say to them if they say like do we really need this plugin architecture because we already are trying to split out our you know service not using

Darius

a core system and so on yeah so like plugin architecture just like fancy kind of name but I guess we more or less doing like similar things already but I think the most important thing is that open close principle that when you kind of need to add new functionality have a task to add new things and when you're kind of doing that and seeing that you need to change like 10 different places so probably you need to kind of reconsider how to improve that the next time you go and change like instead of kind of 10 different places like one or two because the more things you change the probability that something breaks is bigger because if like changing one place or two places like okay like one or two places can break but if you're changing like 10 so there's like probably 10 things that you're kind of touching and could break yeah so at least I think like around like that because it's also like just main principle is just like you have plugin you don't change need to change anything else and if you can achieve that in many different ways thank you

Zino

can I ask my question last question so you were talking about communication being an issue because to me when I think plugins I think like Final Cut Pro plugins I think QuarkXPress plugins like like small units of code that you load in the already running application and so that's where the core system has to be well designed so that you know it doesn't break or does something you don't want to write it's more a speculative kind of question like do you think like this dynamic nature of plugins is going to be used or useful because right now it's for static architecture you compile it and it works and it helps you with the development phase but do you imagine somewhere where you could optionally load some plugins but not others and have more of a dynamic

Darius

kind of thing probably it's hard to say because already there are attempts but small for the games that you can load like different levels kind of separately so it might be that in the apps you have this that you want to have some functionality kind of separate in a form like you name it like module that you kind of load separate and just like put it in when the app is running like games is a good example because you have like different levels that you're kind of loading on the fly so I would assume that it will go to that direction more because the apps are kind of growing and like you don't because they do more like devices do more so at some point it will be just ridiculous to download like half a gigabyte of the app that you need to do kind of single payment because it has like 20 things that you don't use all right thank you

Edit on GitHub