Functional
reload






4 years
Introduction


Introduction


Introduction
Problem

Design stamina hypothesis
Problem
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
SOLID
Problem
T_T ?
Problem

- Aproach
- Outcomes
- Ecosystem
- Real life
Plan
Approach
Functional programming


Approach
Approach
Approach
Approach
Entities
Values
Functions
Approach
Value objects
class Product:
def __init__(self, **kwargs):
self.title = kwargs["title"]
self.isbn = kwargs["isbn"]
Approach
class Product:
def __init__(self, **kwargs):
self.title = kwargs["title"]
self.isbn = kwargs["isbn"]
def slug(self):
return slugify(self.title) + "-" + self.isbn
Hold data
Passive
Immutable
Content
Value objects
Approach
Function objects
import_products = ImportProducts(
download_feed = DownloadFeed(),
products_repo = ProductsRepo()
)
import_products(books_feed)
import_products(dvds_feed)
class ImportProducts:
def __call__(self, feed):
products = self._download_feed(feed)
for attrs in products:
self._products_repo.create(attrs)
class ImportProducts:
def __init__(self, download_feed, products_repo):
self._download_feed = download_feed
self._products_repo = products_repo
def __call__(self, feed):
products = self._download_feed(feed)
for attrs in products:
self._products_repo.create(attrs)
Approach
class ImportProducts:
def __call__(self, feed):
Operate on values
Behavior
Function objects
Approach
Outcomes
Function objects
Outcomes
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
single responsibility principle
"A class should have only one reason to change"
Outcomes
import_products = ImportProducts()
import_products(feed)
Outcomes
single responsibility principle
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
open-closed principle
"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"
Outcomes
ImportProducts
ImportProducts
NotifySubscribers
ImportProducts
NotifySubscribers
UpdateCatalog
Outcomes
open-closed principle
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
Liskov substitution principle
"Subtypes must be substitutable for their base types"
Outcomes
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
Interface segregation principle
"many client-specific interfaces are better than one general-purpose interface."
Outcomes
ImportProducts
DownloadFeed
ImportProducts
DownloadFeed
ReceiveFeedUpload
Outcomes
Interface segregation principle
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
dependency inversion principle
"one should depend upon abstractions, not concretions"
Outcomes
Outcomes
class ImportProducts:
def __init__(self, download_feed, products_repo):
# ...
def __call__(self, feed):
records = self._download_feed(feed)
for attrs in products:
self._products_repo.create(attrs)
dependency inversion principle
SOLID
Outcomes
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
class TestImportProducts(unittest.TestCase):
def setUp(self):
self.download_feed = Mock(DownloadFeed, return_value=[...])
self.repo = Mock(ProductsRepo)
self.subject = ImportProducts(self.download_feed, self.repo)
self.feed = Feed(url='test')
def test_creates_products(self):
self.subject(self.feed)
self.download_feed.assert_called_with(self.feed)
self.repo.create.assert_has_calls([...])
Unit tests
Outcomes
class TestImportProducts(unittest.TestCase):
def setUp(self):
self.download_feed = Mock(DownloadFeed, return_value=[...])
self.repo = Mock(ProductsRepo)
self.subject = ImportProducts(self.download_feed, self.repo)
self.feed = Feed(url='test')
class TestImportProducts(unittest.TestCase):
ImportProducts
DownloadFeed
ProductsRepo
Isolation
Outcomes
Dependencies
Outcomes
Stable deps principle
"depend in the direction of stability"
Outcomes
Less stable
More stable
ImportProducts
DownloadFeed
ProductsRepo
Outcomes
Outcomes
Outcomes
naming
organization
modularization
boundaries
Outcomes
bounded context A
bounded context B
Bounded contexts
Outcomes
Core
HTTP
Persistance
External APIs
Core
HTTP
Persistance
External APIs
Outcomes
better (SOLID) code
better tests
control over dependencies
emerging sub-systems
High-level architecture
Outcomes
class ImportProducts:
def __init__(self, download_feed, products_repo):
self._download_feed = download_feed
self._products_repo = products_repo
def __call__(self, feed):
products = self._download_feed(feed)
for attrs in products:
self._products_repo.create(attrs)



Ecosystem
"Functional Architecture for the Practical Rubyist"
Tim Riley, 2017

Ecosystem
Ecosystem

dry-rb - collection of libraries
rom-rb - object mapper

hanami - web framework

dry-python - set of libraries
Ecosystem
Artiom Malyshev ( /proofit404)
Nikita Sobolev ( /sobolevn)




Alert!
Alert!
Alert!
Alert!
Alert!
Alert!
Alert!
Alert!
Alert!
Ecosystem
require 'dry/struct'
module Types
include Dry::Types.module
end
class User < Dry::Struct
attribute :name, Types::Strict::String.optional
attribute :age, Types::Coercible::Integer
end
user = User.new(name: nil, age: '21')
user.name # => nil
user.age # => 21
Strict types for value objects

Ecosystem
Dependency management


Ecosystem
require 'dry/container'
require 'dry/auto_inject'
module MyApp
# extend MyApp with dependency container functionality
extend Dry::Container::Mixin
# create an injector
Inject = Dry::AutoInject(self)
# register dependencies
register("products_repo") { ProductsRepo.new }
register("download_feed") { DownloadFeed.new }
register("import_products") { ImportProducts.new }
end
class ImportProducts
# will automaticaly create the constructor with all
# deps being injected
include MyApp::Inject["products_repo", "download_feed"]
def call(feed:)
products = download_feed(feed)
products.each do |attrs|
products_repo.create(attrs)
end
end
end
MyApp['import_products'].call(feed: dvds_feed)

namespace('payments') do
namespace('android') do
namespace('in_app_purchases') do
register('apply') { Payments::Android::InAppPurchases::Apply.new }
register('is_inactive') { Payments::Android::InAppPurchases::IsInactive.new }
register('persist') { Payments::Android::InAppPurchases::Persist.new }
register('process') { Payments::Android::InAppPurchases::Process.new }
register('reprocess') { Payments::Android::InAppPurchases::Reprocess.new }
register('receive') { Payments::Android::InAppPurchases::Receive.new }
register('revoke') { Payments::Android::InAppPurchases::Revoke.new }
register('sync') { Payments::Android::InAppPurchases::Sync.new }
end
namespace('subscriptions') do
register('subscribe') { Payments::Android::Subscriptions::Subscribe.new }
register('unsubscribe') { Payments::Android::Subscriptions::Unsubscribe.new }
end
end
namespace('itunes') do
# ...
Dependency management: Python
Ecosystem
Dependency injection


Function composition

Ecosystem
class CreateAccount
def call(params)
if values = validate(params)
if account = create_account(values[:account])
if owner = create_owner(account, values[:owner])
return [account, owner]
else
return [:error, :owner_not_created]
end
else
return [:error, :account_not_created]
end
else
return [:error, :invalid_params]
end
end
private
def validate(params)
# returns true or false
end
# ...
end
require 'dry/monads/result'
require 'dry/monads/do'
class CreateAccount
include Dry::Monads::Result::Mixin
include Dry::Monads::Do.for(:call)
def call(params)
values = yield validate(params)
account = yield create_account(values[:account])
owner = yield create_owner(account, values[:owner])
Success([account, user])
end
private
def validate(params)
# returns Success(values) or Failure(:invalid_params)
end
# ...
end
Business transactions

Ecosystem
class CreateUser
include Dry::Transaction(container: Container)
step :validate, with: "users.validate"
step :create, with: "users.create"
end
create_user = CreateUser.new
create_user.call(name: "Jane", email: "jane@doe.com")
# => Success(#<User name="Jane", email="jane@doe.com">)
Business transactions: Python
Ecosystem
Business transactions DSL


Persistence, web
Ecosystem
rom-rb - object mapper

hanami - web framework
Real life
Receive
Persist
Process
Sync
Apply
GoogleAPIClient
Reprocess
IsInactive
Revoke
FindPlan
Subscribe
Mobile app
Admin
Backgroud job
Android in-app purchases
Unsubscribe
DeactivateExpired
Backgroud job

Subscriptions
Android
user ids
subscription plan ids
subscribe(uid, plan_id)
unsubscribe(uid, plan_id)
Android in-app purchases
Real life
Problems
Real life
- Code arrangement takes more time
- Possibility of overcomplication
- Lack of IDE support
Links



Happy new year!
Функциональная перезагрузка
By vizvamitra
Функциональная перезагрузка
- 1,405