Refactor Firefly library #1588
Replies: 4 comments 7 replies
-
Great writeup Matt. A couple of opinions of mine:
|
Beta Was this translation helpful? Give feedback.
-
I don’t really have a strong opinion about this though. If we agree to have
Very good proposal. I think once we have an agreement from everyone on this, the next step would be to plan how to best approach these changes. |
Beta Was this translation helpful? Give feedback.
-
[WIP] Firefly Refactor IntegrationThere needs to be a plan going forward in integrating this refactor into the existing repository as it is quite substantial. There are all sorts of ways to organize this work, parallelizing and serializing some tasks in a certain configuration, so if you have any thoughts please let it be known 🙂 Some ThingsIt would be nice to apply some things in general when doing the refactor to help clean the code. I believe for some of these there may be tools to enforce them (if we all like them and agree 👍) to make it easier, but anyways.
The PlanIt is best if this refactor is split into both logical and manageable phases for us so we can get it done efficiently. TODO Phase 01 -
|
Beta Was this translation helpful? Give feedback.
-
I have started writing a finalized proposal for this refactor here (HackMD). |
Beta Was this translation helpful? Give feedback.
-
Firefly
lib
Refactor - OverviewOther Docs
core
functionalityOverview
With the upcoming features planned for the network (e.g. voting, digital assets) it'll be important to place Firefly in a position where it is more easily extendable to support user interaction for said features.
At the moment we have most of the business logic in
shared/lib
in a flat hierarchy.This sometimes leads to files with 1000+ lines of code where it becomes difficult to find or modify functions, determine best placement for a new function, etc. It also makes it more likely for a developer to re-implement a function that is already there.
This restructure proposal defines a more hierarchical structure to
shared/lib
that will allow the code to be more easily extendable and maintainable while also being DRY, loosely-coupled, and modular. The files will be smaller and more specifically named, making it easier to locate functionality that one may need.Modules
The basic structure will be made of pseduo-modules, or module-like directories containing source code for a particular mechanism, system, or functionality.
At the heart will be the
core
module, containing functionality that is central to the app in some way (e.g. routing, platform-based logic, i18n). The next module iscommon
, which is to be used by any<feature>
module. The<feature>
modules are isolated collections of source code for a particular functionality in Firefly (e.g. digital assets, voting).Structure
The basic structure of a module will look like:
* Right now we are using
interface
s for both purely data objects and purely functional objects. It might be best to separate these and usetype
s for data-only andinterface
s for functional objects.Modules should support nesting, although for the sake of clean and concise imports it is (probably) best to use a maximum folder-nesting depth of 2*:
* This is likely for specific cases either where the source files are large (i.e.
migration.ts
andwallet.ts
) or the functionality is going to be within thecore
module (i.e.app
,network
).Responsibilities
core
lib/wallet.ts
)lib/shell
common
digital-assets
voting
(this assumes a standalone feature rather than a simpler implementation)* It is unsure how the
wallet.rs
API will look for these, especially as it's being redesigned at the moment.Organization
The following directory structure will be in
packages/shared/lib
:core/
app/
constants/
updater.ts
stores/
app.store.ts
settings.store.ts
updater.store.ts
tests/
*.(test|spec).ts
...
types/
app.type.ts
app.ts
settings.ts
updater.ts
constants/
time.ts
shell/
error-logger.ts
wallet-api.ts
wallet-errors.ts
stores/
router.store.ts
tests/
*.(test|spec).ts
...
types/
electron.type.ts
error.type.ts
event.type.ts
i18n.type.ts
icon.type.ts
notification.type.ts
route.type.ts
actions.ts
actor.ts
bridge.ts
electron.ts
errors.ts
helpers.ts
i18n.ts
router.ts
utils.ts
validator.ts
common/
constants/
chart.ts
market.ts
profile.ts
MAX_PROFILE_NAME_LENGTH
is inlib/wallet.ts
.network/
constants/
network.ts
node.ts
lib/migration.ts
.stores/
status.store.ts
tests/
*.(test|spec).ts
...
types/
client.type.ts
message.type.ts
network.type.ts
node.type.ts
network.ts
status.ts
stores/
chart.store.ts
market.store.ts
notification.store.ts
popup.store.ts
profile.store.ts
walletPin
, which 1) should be renamed toprofilePin
and 2) is currently inlib/app.ts
.walletSetupType
, which 1) should be renamed toprofileSetupType
and 2) is currently inlib/router.ts
.tests/
*.(test|spec).ts
...
types/
chart.type.ts
icon.type.ts
market.type.ts
profile.type.ts
wallet/
*constants/
account.ts
dust.ts
stronghold.ts
storage.ts
migration/
constants/
bundle.ts
migration.ts
stores/
migration.store.ts
tests/
*.(test|spec).ts
...
types/
bundle.type.ts
migration.type.ts
bundle.ts
migration.ts
stores/
ledger.store.ts
stronghold.store.ts
password.store.ts
wallet.store.ts
sendParams
needs to be in this file instead ofapp.ts
, although not sure if this will affect deep linking.tests/
*.(test|spec).ts
...
types/
account.type.ts
address.type.ts
currency.type.ts
ledger.type.ts
migration.type.ts
mnemonic.type.ts
output.type.ts
account.ts
api.ts
bech32.ts
currency.ts
ledger.ts
mnemonic.ts
password.ts
stronghold.ts
storage.ts
units.ts
chart.ts
deep-linking.ts
common
.market.ts
notifications.ts
popup.ts
profile.ts
digital-assets/
...
voting/
...
* I initially had this as a
<feature>
module, but I think it belongs incommon
since it will most likely be used by other features such as voting, digital assets, a contact system, etc.CAUTION: Please take note of the following:
core
must NOT depend on anything unless it is also incore
.common
must NOT depend on any<feature>
modules; onlycore
.<feature>
modules must NOT depend on any other<feature>
modules; onlycommon
,core
, or the same moduleRelationships
TODO: Create module relationship / dependency diagram
I am also proposing that we start using
interface
s for grouping collections of functions, which will...Single Responsibility Principle (SRP): modules and source files should have one reason to change and should be responsible for only one actor. If a source code file or module is subject to change because of multiple reasons (could be b/c of other functions, or a bug, i.e. anything), then it means that there is logic that should be separated.
It is easiest to do this coming up with a data flow for a process within the application. In Firefly, transactions are displayed in the wallet dashboard, however we have been talking about adding a feature for exporting them as
.pdf
,.csv
, etc. We could say that there are two processes for this: 1) syncing the account transactions and 2) presenting that data in a browser- and printer-friendly format. The two objects here could beAccountSynchronizer
, containing logic for syncing an account, namely its messages, andMessagePresenter
, containing logic for displaying or exporting the data in a particular format. Perhaps aMessageExporter
is used by theMessagePresenter
containing different logic for the different formats (HTML, PDF, CSV, etc.).Open-Closed Principle (OCP): modules and source files should be open for extension but closed for modification. This means that adding new features to Firefly should result in little (ideally none) modification of existing functionality. This is accomplished this by separating modules and source code that changes for different reasons (the SRP) then properly structuring the dependencies them (the Dependency Inversion Principle or DIP).
Liskov Substitution Principle (LSP): if an an object of type
S
can be swapped with in object of typeT
in a programP
where the behavior ofP
remains the same thenS
is a subtype ofT
. The aim of LSP is to create as much interoperability between types as possible, which results in adding fewer mechanisms (i.e. checks, guards, etc.) in the logic for handling different types.I think a great example of this in Firefly is how we check for the type of a profile when doing different things. This makes more sense in the Svelte components as it's UI-related logic and less so in other parts of the business logic. It would quite fantastic if we were able to specify the profile type and just call a function that is defined by separate interfaces for Ledger and software profiles. Doing so would keep the code less polluted with extra unneeded logic in places, putting it in grouped
interface
s.Interface Segregation Principle (ISP): this refers to isolating operations into
interface
s that exist between the operations themselves and the actors that use them. This helps us avoid inadvertent dependencies on source code that we don't necessarily need.Dependency Inversion Principle (DIP): when source code dependencies refer only to abstractions and not concretions is when a system can be most flexible. This cannot be applied all the time, so it's worth noting that this particularly applies to volatile concrete objects, which are artifacts being actively developed or some type of frequent change.
This one can be broken down into a few specific coding practices:
It is important that we try and adhere to S.O.L.I.D principles when making this refactor to help keep the architecture clean. The five points are:
Benefits
lib
on the npm registry.Drawbacks / Concerns
core
really needs to be the core functionality needed for the app. Let's be cautious that it doesn't become a bloated module / directory.import { ... } from 'shared/lib/wallet/migration/migration.ts'
instead ofimport { ... } from 'shared/lib/wallet/migration'
@lib/...
is enough IMO, but we can also make them shorter and more specific such as@core/...
and@common/network/...
logout()
may potentially be problematic if it's incore
(orcommon
) because it resets a bunch of state across the entire app. We'll need to figure out a way to break this dependency somehow or put the logic in a better place (is there a better place?)Questions
Not sure of which term to use or how to group the three main assets planned for the network: IOTA tokens, colored coins, and NFTs...
wallet
(IMO) doesn't sound quite right when being used to mean all three; it feels more like it represents strictly money rather than other assets.portfolio
is a nice word to use that works for NFTs and colored coins, perhaps even IOTA tokens too. Howeverwallet
could also be seen as meaning the collection of transactable tokens, which would mean colored coins and IOTA tokens; NFTs are intended more for collecting rather than transacting necessarily.Should the code for
wallet
also be a<feature>
module? I think it depends how the upcoming features will be implemented into Firefly, i.e. if they are separate features with dedicated pages / tabs (left-hand side) then they should be standalone<feature>
modules. However since functionality inwallet
would be used elsewhere then it should be incommon
right?We should stick to one method of using the
wallet.rs
API; right now there are instances where it's called directly from both other business logic and UI components usingapi.syncAccounts(...)
or indirectly with a wrapper likeasyncSyncAccounts(...)
.With wrappers, it'll be easier to hide implementation details, which is important, however having an
api
object is nice as we can more easily see available functionality (and handle different outcomes for the same API call if needed). In cases where functionality is duplicated, it's probably best defined as a function to make it DRY-er, but at the same time having a consistent method of API implementation is ideal.Other Improvements
app.ts
:network.ts
:Beta Was this translation helpful? Give feedback.
All reactions