Functional

reload

Dmitrii Krasnov

@vizvamitra

/vizvamitra  

4 years

Introduction

Introduction

Introduction

code \rightarrow mess
codemesscode \rightarrow mess

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

f: x \rightarrow y
f:xyf: x \rightarrow y

Approach

app: request \rightarrow response
app:requestresponseapp: request \rightarrow response

Approach

req \rightarrow [*] \rightarrow ... \rightarrow [*] \rightarrow resp
req[]...[]respreq \rightarrow [*] \rightarrow ... \rightarrow [*] \rightarrow resp

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

app: request \rightarrow response
app:requestresponseapp: request \rightarrow response
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

https://youtu.be/7qnsRejCyEQ

Ecosystem

Ecosystem

dry-rb - collection of libraries

dry-rb.org

rom-rb - object mapper

rom-rb.org

hanami - web framework

hanamirb.org

dry-python - set of libraries

github.com/dry-python

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

   /dry-python/dependencies

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

   /dry-python/stories

Persistence, web

Ecosystem

rom-rb - object mapper

rom-rb.org

hanami - web framework

hanamirb.org

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,217