Slide 1

Slide 1 text

Hynek Schlawack @[email protected] • @hynek Subclassing, Composition, Python, & You.

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

class Base: pass class Sub(Base): pass Subclassing

Slide 6

Slide 6 text

class Base: pass class Sub(Base): pass Subclassing

Slide 7

Slide 7 text

@dataclass class Base: x: int class Sub(Base): y = 0 def compute(self): self.y += self.x * 2

Slide 8

Slide 8 text

@dataclass class Base: x: int def cool_new_feature(self): self.y = "LOL" class Sub(Base): y = 0 def compute(self): self.y += self.x * 2

Slide 9

Slide 9 text

Bidirectional Relationship

Slide 10

Slide 10 text

Composition

Slide 11

Slide 11 text

@dataclass class A: b: B y: int = 0 Composition

Slide 12

Slide 12 text

@dataclass class A: b: B y: int = 0 Composition

Slide 13

Slide 13 text

@dataclass class A: b: B y: int = 0 def compute(self): self.y += self.b.x * 2 Composition

Slide 14

Slide 14 text

@dataclass class A: b: B y: int = 0 def compute(self): self.y += self.b.x * 2 Composition

Slide 15

Slide 15 text

Why do we subclass?

Slide 16

Slide 16 text

Code Sharing

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Goose Chase Program Flow™

Slide 21

Slide 21 text

class A: def method(self): ... class B: def method(self): ... class C: def method(self): ... class You(A, B, C): def foo(self): self.method()

Slide 22

Slide 22 text

class A: def method(self): self.bar() class B(A): def bar(self): ... class C(B): def bar(self): ... class You(C): def foo(self): self.method()

Slide 23

Slide 23 text

class A: def method(self): self.bar() class B(A): def bar(self): ... class C(B): def bar(self): ... class You(C): def foo(self): self.method()

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

— Architecture Patterns with Python, page 131

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

Repository

Slide 28

Slide 28 text

Tracking Repository

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set()

Slide 31

Slide 31 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set() @abc.abstractmethod def _add(self, product): raise NotImplementedError @abc.abstractmethod def _get(self, sku): raise NotImplementedError

Slide 32

Slide 32 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set() def add_product(self, product): self._add(product) self.seen.add(product) @abc.abstractmethod def _add(self, product): raise NotImplementedError @abc.abstractmethod def _get(self, sku): raise NotImplementedError

Slide 33

Slide 33 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set() def add_product(self, product): self._add(product) self.seen.add(product) def get_by_sku(self, sku): if product := self._get(sku): self.seen.add(product) return product @abc.abstractmethod def _add(self, product): raise NotImplementedError @abc.abstractmethod def _get(self, sku): raise NotImplementedError

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

class FakeTrackingRepository(AbstractTrackingRepository): def __init__(self, products): super().__init__() self._products = set(products)

Slide 36

Slide 36 text

class FakeTrackingRepository(AbstractTrackingRepository): def __init__(self, products): super().__init__() self._products = set(products) def _add(self, product): self._products.add(product) def _get(self, sku): return next(( p for p in self._products if p.sku == sku), None)

Slide 37

Slide 37 text

class FakeTrackingRepository(AbstractTrackingRepository): def __init__(self, products): super().__init__() self._products = set(products) def _add(self, product): self._products.add(product) def _get(self, sku): return next(( p for p in self._products if p.sku == sku), None)

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set() def add_product(self, product): self._add(product) self.seen.add(product) def get_by_sku(self, sku): if product := self._get(sku): self.seen.add(product) return product @abc.abstractmethod def _add(self, product): raise NotImplementedError @abc.abstractmethod def _get(self, sku): raise NotImplementedError

Slide 40

Slide 40 text

class AbstractTrackingRepository(abc.ABC): def __init__(self): self.seen = set() def add_product(self, product): self._add(product) self.seen.add(product) def get_by_sku(self, sku): if product := self._get(sku): self.seen.add(product) return product @abc.abstractmethod def _add(self, product): raise NotImplementedError @abc.abstractmethod def _get(self, sku): raise NotImplementedError

Slide 41

Slide 41 text

class AbstractRepository(abc.ABC): @abc.abstractmethod def add(self, product: Product) -> None: ... @abc.abstractmethod def get(self, sku: str) -> Product | None: ...

Slide 42

Slide 42 text

class AbstractRepository(abc.ABC): @abc.abstractmethod def add(self, product: Product) -> None: ... @abc.abstractmethod def get(self, sku: str) -> Product | None: ... Nominal subtyping

Slide 43

Slide 43 text

class AbstractRepository(abc.ABC): @abc.abstractmethod def add(self, product: Product) -> None: ... @abc.abstractmethod def get(self, sku: str) -> Product | None: ... Nominal subtyping (metaclass=abc.ABCMeta):

Slide 44

Slide 44 text

@typing.runtime_checkable class Repository(typing.Protocol): def add(self, product: Product) -> None: ... def get(self, sku: str) -> Product | None: ...

Slide 45

Slide 45 text

@typing.runtime_checkable class Repository(typing.Protocol): def add(self, product: Product) -> None: ... def get(self, sku: str) -> Product | None: ...

Slide 46

Slide 46 text

@typing.runtime_checkable class Repository(typing.Protocol): def add(self, product: Product) -> None: ... def get(self, sku: str) -> Product | None: ... Structural subtyping

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

class FakeRepository: def __init__(self, products): self._products = set(products)

Slide 49

Slide 49 text

class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product)

Slide 50

Slide 50 text

class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product)

Slide 51

Slide 51 text

class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product) def get(self, sku: str) -> Product | None: return next(( p for p in self._products if p.sku == sku), None)

Slide 52

Slide 52 text

class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product) def get(self, sku: str) -> Product | None: return next(( p for p in self._products if p.sku == sku), None)

Slide 53

Slide 53 text

class FakeRepository(AbstractRepository): def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product) def get(self, sku: str) -> Product | None: return next(( p for p in self._products if p.sku == sku), None)

Slide 54

Slide 54 text

class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product) def get(self, sku: str) -> Product | None: return next(( p for p in self._products if p.sku == sku), None) AbstractRepository.register(FakeRepository)

Slide 55

Slide 55 text

@AbstractRepository.register class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product: Product ) -> None: self._products.add(product) def get(self, sku: str) -> Product | None: return next(( p for p in self._products if p.sku == sku), None)

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

@dataclass class TrackingRepository: _repo: Repository seen: set[Product] = field(default_factory=set)

Slide 58

Slide 58 text

@dataclass class TrackingRepository: _repo: Repository seen: set[Product] = field(default_factory=set) def add_product(self, product: Product) -> None: self._repo.add(product) self.seen.add(product)

Slide 59

Slide 59 text

@dataclass class TrackingRepository: _repo: Repository seen: set[Product] = field(default_factory=set) def add_product(self, product: Product) -> None: self._repo.add(product) self.seen.add(product) def get_by_sku(self, sku: str) -> Product | None: if product := self._repo.get(sku): self.seen.add(product) return product

Slide 60

Slide 60 text

@dataclass class TrackingRepository: _repo: Repository seen: set[Product] = field(default_factory=set) def add_product(self, product): self._repo.add(product) self.seen.add(product) def get_by_sku(self, sku): if product := self._repo.get(sku): self.seen.add(product) return product class FakeRepository: def __init__(self, products): self._products = set(products) def add(self, product): self._products.add(product) def get(self, sku): return next( ( p for p in self._products if p.sku == sku ), None )

Slide 61

Slide 61 text

Strategy Pattern

Slide 62

Slide 62 text

Testing

Slide 63

Slide 63 text

Subclassing requires knowledge + self-discipline

Slide 64

Slide 64 text

Composition mechanically enforces discipline Subclassing requires knowledge + self-discipline

Slide 65

Slide 65 text

Muddling of Namespaces

Slide 66

Slide 66 text

Namespaces are one honking great idea – let’s do more of those! — Uncle Timmy, Zen of Python, PEP 20

Slide 67

Slide 67 text

Namespace Pollution

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

def decorator(cls): cls.a = 42 return cls

Slide 70

Slide 70 text

def decorator(cls): cls.a = 42 return cls @decorator class C: pass

Slide 71

Slide 71 text

def decorator(cls): cls.a = 42 return cls @decorator class C: pass assert hasattr(C, "a")

Slide 72

Slide 72 text

def decorator(cls): cls.a = 42 return cls @decorator class C: pass assert hasattr(C, "a") == C = decorator(_C)

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

class Question(django.db.models.Model): question_text = CharField(max_length=200) pub_date = DateTimeField('date published')

Slide 75

Slide 75 text

class Question(django.db.models.Model): question_text = CharField(max_length=200) pub_date = DateTimeField('date published') @dataclass class Question: question_text: str pub_date: datetime @attrs.define

Slide 76

Slide 76 text

class Question(django.db.models.Model): question_text = CharField(max_length=200) pub_date = DateTimeField('date published') @dataclass class Question: question_text: str pub_date: datetime

Slide 77

Slide 77 text

['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__match_args__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'pub_date', 'question_text'] dir(Question("Hi!", today())) – dataclass Edition

Slide 78

Slide 78 text

['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__match_args__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'pub_date', 'question_text'] dir(Question("Hi!", today())) – dataclass Edition 32

Slide 79

Slide 79 text

['DoesNotExist', 'MultipleObjectsReturned', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_check_column_name_clashes', '_check_constraints', '_check_default_pk', '_check_field_name_clashes', '_check_fields', '_check_id_field', '_check_index_together', '_check_indexes', '_check_local_fields', '_check_long_column_names', '_check_m2m_through_same_relationship', '_check_managers', '_check_model', '_check_model_name_db_lookup_clashes', '_check_ordering', '_check_property_name_related_field_accessor_clashes', '_check_single_primary_key', '_check_swappable', '_check_unique_together', '_do_insert', '_do_update', '_get_FIELD_display', '_get_expr_references', '_get_field_value_map', '_get_next_or_previous_by_FIELD', '_get_next_or_previous_in_order', '_get_pk_val', '_get_unique_checks', '_meta', '_perform_date_checks', '_perform_unique_checks', '_prepare_related_fields_for_save', '_save_parents', '_save_table', '_set_pk_val', '_state', 'check', 'choice_set', 'clean', 'clean_fields', 'date_error_message', 'delete', 'from_db', 'full_clean', 'get_constraints', 'get_deferred_fields', 'get_next_by_pub_date', 'get_previous_by_pub_date', 'id', 'objects', 'pk', 'prepare_database_save', 'pub_date', 'question_text', 'refresh_from_db', 'save', 'save_base', 'serializable_value', 'unique_error_message', 'validate_constraints', 'validate_unique'] dir(Question("Hi!", today())) – ORM Edition

Slide 80

Slide 80 text

['DoesNotExist', 'MultipleObjectsReturned', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_check_column_name_clashes', '_check_constraints', '_check_default_pk', '_check_field_name_clashes', '_check_fields', '_check_id_field', '_check_index_together', '_check_indexes', '_check_local_fields', '_check_long_column_names', '_check_m2m_through_same_relationship', '_check_managers', '_check_model', '_check_model_name_db_lookup_clashes', '_check_ordering', '_check_property_name_related_field_accessor_clashes', '_check_single_primary_key', '_check_swappable', '_check_unique_together', '_do_insert', '_do_update', '_get_FIELD_display', '_get_expr_references', '_get_field_value_map', '_get_next_or_previous_by_FIELD', '_get_next_or_previous_in_order', '_get_pk_val', '_get_unique_checks', '_meta', '_perform_date_checks', '_perform_unique_checks', '_prepare_related_fields_for_save', '_save_parents', '_save_table', '_set_pk_val', '_state', 'check', 'choice_set', 'clean', 'clean_fields', 'date_error_message', 'delete', 'from_db', 'full_clean', 'get_constraints', 'get_deferred_fields', 'get_next_by_pub_date', 'get_previous_by_pub_date', 'id', 'objects', 'pk', 'prepare_database_save', 'pub_date', 'question_text', 'refresh_from_db', 'save', 'save_base', 'serializable_value', 'unique_error_message', 'validate_constraints', 'validate_unique'] dir(Question("Hi!", today())) – ORM Edition 91

Slide 81

Slide 81 text

class QuestionsRepository: conn: sqlalchemy.engine.Connection def get(self, id: int) -> Question: row := self.conn.execute( select( tables.questions.c.pub_date ).where( tables.questions.c.id == id ) ).one() return Question(id=id, pub_date=row.pub_date)

Slide 82

Slide 82 text

class QuestionsRepository: conn: sqlalchemy.engine.Connection def get(self, id: int) -> Question: row := self.conn.execute( select( tables.questions.c.pub_date ).where( tables.questions.c.id == id ) ).one() return Question(id=id, pub_date=row.pub_date)

Slide 83

Slide 83 text

class QuestionsRepository: conn: sqlalchemy.engine.Connection def get(self, id: int) -> Question: row := self.conn.execute( select( tables.questions.c.pub_date ).where( tables.questions.c.id == id ) ).one() return Question(id=id, pub_date=row.pub_date)

Slide 84

Slide 84 text

['DoesNotExist', 'MultipleObjectsReturned', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_check_column_name_clashes', '_check_constraints', '_check_default_pk', '_check_field_name_clashes', '_check_fields', '_check_id_field', '_check_index_together', '_check_indexes', '_check_local_fields', '_check_long_column_names', '_check_m2m_through_same_relationship', '_check_managers', '_check_model', '_check_model_name_db_lookup_clashes', '_check_ordering', '_check_property_name_related_field_accessor_clashes', '_check_single_primary_key', '_check_swappable', '_check_unique_together', '_do_insert', '_do_update', '_get_FIELD_display', '_get_expr_references', '_get_field_value_map', '_get_next_or_previous_by_FIELD', '_get_next_or_previous_in_order', '_get_pk_val', '_get_unique_checks', '_meta', '_perform_date_checks', '_perform_unique_checks', '_prepare_related_fields_for_save', '_save_parents', '_save_table', '_set_pk_val', '_state', 'check', 'choice_set', 'clean', 'clean_fields', 'date_error_message', 'delete', 'from_db', 'full_clean', 'get_constraints', 'get_deferred_fields', 'get_next_by_pub_date', 'get_previous_by_pub_date', 'id', 'objects', 'pk', 'prepare_database_save', 'pub_date', 'question_text', 'refresh_from_db', 'save', 'save_base', 'serializable_value', 'unique_error_message', 'validate_constraints', 'validate_unique']

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

Readability

Slide 87

Slide 87 text

Readability Control

Slide 88

Slide 88 text

Readability Convenience Control

Slide 89

Slide 89 text

Readability Convenience Control Performance

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

Seth Michael Larson Working on urllib3 full-time for one week

Slide 92

Slide 92 text

Programs are meant to be read by humans and only incidentally for computers to execute. — Abelson & Sussman, SICP Seth Michael Larson Working on urllib3 full-time for one week

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

Functions

Slide 96

Slide 96 text

? class A: b: B def other_method(): ... def method(self): self.other_method() self.b.even_other_method() class B: a: A def even_other_method(): ... def method(self): self.a.other_method() self.even_other_method() or

Slide 97

Slide 97 text

class A: b: B def other_method(): ... def method(self): self.other_method() self.b.even_other_method() class B: a: A def even_other_method(): ... def method(self): self.a.other_method() self.even_other_method() or def function(a, b): a.other_method() b.even_other_method()

Slide 98

Slide 98 text

class C: a: A b: B def method(self): self.a.other_method() self.b.even_other_method()

Slide 99

Slide 99 text

Why anyway!?

Slide 100

Slide 100 text

Metaclasses

Slide 101

Slide 101 text

Exceptions

Slide 102

Slide 102 text

class MyError(Exception): pass Exceptions

Slide 103

Slide 103 text

class MyError(Exception): pass class MyError(ValueError, KeyError): pass Exceptions

Slide 104

Slide 104 text

class MyError(Exception): pass class MyError(ValueError, KeyError): pass Exceptions

Slide 105

Slide 105 text

Specialization

Slide 106

Slide 106 text

E-Mail Addresses

Slide 107

Slide 107 text

class Mailbox: id: UUID addr: str pwd: str E-Mail Addresses

Slide 108

Slide 108 text

class Mailbox: id: UUID addr: str pwd: str class Forwarder: id: UUID addr: str targets: list[str] E-Mail Addresses

Slide 109

Slide 109 text

class Mailbox: id: UUID addr: str pwd: str class Forwarder: id: UUID addr: str targets: list[str] E-Mail Addresses EmailAddr = Forwarder | Mailbox

Slide 110

Slide 110 text

class Mailbox: id: UUID addr: str pwd: str class Forwarder: id: UUID addr: str targets: list[str] E-Mail Addresses EmailAddr = Forwarder | Mailbox

Slide 111

Slide 111 text

Duplication is far cheaper than a wrong abstraction. — Sandi Metz

Slide 112

Slide 112 text

Create one class, make fields optional

Slide 113

Slide 113 text

class AddrType(enum.Enum): MAILBOX = "mailbox" FORWARDER = "forwarder" class EmailAddr: type: AddrType id: UUID addr: str Create one class, make fields optional

Slide 114

Slide 114 text

class AddrType(enum.Enum): MAILBOX = "mailbox" FORWARDER = "forwarder" class EmailAddr: type: AddrType id: UUID addr: str # Only useful if type == AddrType.MAILBOX pwd: str | None # Only useful if type == AddrType.FORWARDER target: list[str] | None Create one class, make fields optional

Slide 115

Slide 115 text

class AddrType(enum.Enum): MAILBOX = "mailbox" FORWARDER = "forwarder" class EmailAddr: type: AddrType id: UUID addr: str # Only useful if type == AddrType.MAILBOX pwd: str | None # Only useful if type == AddrType.FORWARDER target: list[str] | None Create one class, make fields optional

Slide 116

Slide 116 text

Composition

Slide 117

Slide 117 text

class EmailAddr: id: UUID addr: str Composition

Slide 118

Slide 118 text

class EmailAddr: id: UUID addr: str class Mailbox: email: EmailAddr pwd: str Composition

Slide 119

Slide 119 text

class EmailAddr: id: UUID addr: str class Mailbox: email: EmailAddr pwd: str class Forwarder: email: EmailAddr targets: list[str] Composition

Slide 120

Slide 120 text

class EmailAddr: id: UUID addr: str class Mailbox: email: EmailAddr pwd: str class Forwarder: email: EmailAddr targets: list[str] Composition

Slide 121

Slide 121 text

class EmailAddr: id: UUID addr: str class Mailbox: email: EmailAddr pwd: str class Forwarder: email: EmailAddr targets: list[str] Composition

Slide 122

Slide 122 text

Create a Common Base Class, Then Specialize

Slide 123

Slide 123 text

class EmailAddr: id: UUID addr: str Create a Common Base Class, Then Specialize

Slide 124

Slide 124 text

class EmailAddr: id: UUID addr: str class Mailbox(EmailAddr): pwd: str class Forwarder(EmailAddr): targets: list[str] Create a Common Base Class, Then Specialize

Slide 125

Slide 125 text

class EmailAddr: id: UUID addr: str class Mailbox(EmailAddr): pwd: str class Forwarder(EmailAddr): targets: list[str] Create a Common Base Class, Then Specialize

Slide 126

Slide 126 text

No content

Slide 127

Slide 127 text

type EmailAddr struct { addr string } type Mailbox struct { EmailAddr pwd string }

Slide 128

Slide 128 text

type EmailAddr struct { addr string } type Mailbox struct { EmailAddr pwd string } func main() { mbox := Mailbox{EmailAddr{"[email protected]"}, "a hash"} fmt.Println(mbox.addr) }

Slide 129

Slide 129 text

type EmailAddr struct { addr string } type Mailbox struct { EmailAddr pwd string } func main() { mbox := Mailbox{EmailAddr{"[email protected]"}, "a hash"} fmt.Println(mbox.addr) }

Slide 130

Slide 130 text

No content

Slide 131

Slide 131 text

No content

Slide 132

Slide 132 text

No content

Slide 133

Slide 133 text

No content

Slide 134

Slide 134 text

I never allow myself to have an opinion on anything that I don’t know the other side’s argument better than they do. — Charlie Munger

Slide 135

Slide 135 text

No content

Slide 136

Slide 136 text

OX.CX/SC

Slide 137

Slide 137 text

OX.CX/SC @HYNEK (@MASTODON.SOCIAL)

Slide 138

Slide 138 text

OX.CX/SC @HYNEK (@MASTODON.SOCIAL) VRMD.DE