Run prettier everywhere
This commit is contained in:
parent
6f941a8ab3
commit
c328d43192
@ -1,18 +1,18 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Antipatterns](#antipatterns)
|
- [Antipatterns](#antipatterns)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Antipatterns
|
# Antipatterns
|
||||||
============
|
|
||||||
|
|
||||||
This is a list of antipatterns.
|
This is a list of antipatterns.
|
||||||
|
|
||||||
* [Code antipatterns](./code-antipatterns.md)
|
- [Code antipatterns](./code-antipatterns.md)
|
||||||
* [Python antipatterns](./python-antipatterns.md)
|
- [Python antipatterns](./python-antipatterns.md)
|
||||||
* [SQLAlchemy antipatterns](./sqlalchemy-antipatterns.md)
|
- [SQLAlchemy antipatterns](./sqlalchemy-antipatterns.md)
|
||||||
* [Error handling antipatterns](./error-handling-antipatterns.md)
|
- [Error handling antipatterns](./error-handling-antipatterns.md)
|
||||||
* [Tests antipatterns](./tests-antipatterns.md)
|
- [Tests antipatterns](./tests-antipatterns.md)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Antipatterns](#antipatterns)
|
- [Antipatterns](#antipatterns)
|
||||||
@ -25,8 +26,7 @@
|
|||||||
Most of those are antipatterns in the Python programming language, but some of
|
Most of those are antipatterns in the Python programming language, but some of
|
||||||
them might be more generic.
|
them might be more generic.
|
||||||
|
|
||||||
Strict email validation
|
## Strict email validation
|
||||||
-----------------------
|
|
||||||
|
|
||||||
It is almost impossible to strictly validate an email. Even if you were writing
|
It is almost impossible to strictly validate an email. Even if you were writing
|
||||||
or using a regex that follows
|
or using a regex that follows
|
||||||
@ -41,8 +41,7 @@ To sum up, don't waste your time trying to validate an email if you don't need
|
|||||||
to (or just check that there's a `@` in it). If you need to, send an email with
|
to (or just check that there's a `@` in it). If you need to, send an email with
|
||||||
a token and validate that the user received it.
|
a token and validate that the user received it.
|
||||||
|
|
||||||
Late returns
|
## Late returns
|
||||||
------------
|
|
||||||
|
|
||||||
Returning early reduces cognitive overhead, and improve readability by killing
|
Returning early reduces cognitive overhead, and improve readability by killing
|
||||||
indentation levels.
|
indentation levels.
|
||||||
@ -66,8 +65,7 @@ def toast(bread):
|
|||||||
toaster.toast(bread)
|
toaster.toast(bread)
|
||||||
```
|
```
|
||||||
|
|
||||||
Hacks comment
|
## Hacks comment
|
||||||
-------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -78,13 +76,13 @@ toaster.restart()
|
|||||||
|
|
||||||
There's multiple things wrong with this comment:
|
There's multiple things wrong with this comment:
|
||||||
|
|
||||||
* Even if it is actually a hack, no need to say it in a comment. It lowers the
|
- Even if it is actually a hack, no need to say it in a comment. It lowers the
|
||||||
perceived quality of a codebase and impacts developer motivation.
|
perceived quality of a codebase and impacts developer motivation.
|
||||||
* Putting the author and the date is totally useless when using source control
|
- Putting the author and the date is totally useless when using source control
|
||||||
(`git blame`).
|
(`git blame`).
|
||||||
* This does not explain why it's temporary.
|
- This does not explain why it's temporary.
|
||||||
* It's impossible to easily grep for temporary fixes.
|
- It's impossible to easily grep for temporary fixes.
|
||||||
* [Louis de Funès](https://en.wikipedia.org/wiki/Louis_de_Fun%C3%A8s) would never
|
- [Louis de Funès](https://en.wikipedia.org/wiki/Louis_de_Fun%C3%A8s) would never
|
||||||
write a hack.
|
write a hack.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
@ -95,10 +93,10 @@ Good:
|
|||||||
toaster.restart()
|
toaster.restart()
|
||||||
```
|
```
|
||||||
|
|
||||||
* This clearly explains the nature of the temporary fix.
|
- This clearly explains the nature of the temporary fix.
|
||||||
* Using `TODO` is an ubiquitous pattern that allows easy grepping and plays
|
- Using `TODO` is an ubiquitous pattern that allows easy grepping and plays
|
||||||
nice with most text editors.
|
nice with most text editors.
|
||||||
* The perceived quality of this temporary fix is much higher.
|
- The perceived quality of this temporary fix is much higher.
|
||||||
|
|
||||||
## Repeating arguments in function name
|
## Repeating arguments in function name
|
||||||
|
|
||||||
@ -146,8 +144,7 @@ Which produces much more concise code:
|
|||||||
toaster = Toasters.get(1)
|
toaster = Toasters.get(1)
|
||||||
```
|
```
|
||||||
|
|
||||||
Repeating function name in docstring
|
## Repeating function name in docstring
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -159,8 +156,8 @@ def test_return_true_if_toast_is_valid():
|
|||||||
|
|
||||||
Why is it bad?
|
Why is it bad?
|
||||||
|
|
||||||
* The docstring and function name are not DRY.
|
- The docstring and function name are not DRY.
|
||||||
* There's no actual explanation of what valid means.
|
- There's no actual explanation of what valid means.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
|
|
||||||
@ -177,8 +174,7 @@ def test_brioche_are_valid_toast():
|
|||||||
assert is_valid(Toast('brioche')) is true
|
assert is_valid(Toast('brioche')) is true
|
||||||
```
|
```
|
||||||
|
|
||||||
Unreadable response construction
|
## Unreadable response construction
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
@ -207,8 +203,7 @@ def get_data():
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Undeterministic tests
|
## Undeterministic tests
|
||||||
---------------------
|
|
||||||
|
|
||||||
When testing function that don't behave deterministically, it can be tempting
|
When testing function that don't behave deterministically, it can be tempting
|
||||||
to run them multiple time and average their results.
|
to run them multiple time and average their results.
|
||||||
@ -235,11 +230,11 @@ def test_function():
|
|||||||
|
|
||||||
There are multiple things that are wrong with this approach:
|
There are multiple things that are wrong with this approach:
|
||||||
|
|
||||||
* This is a flaky test. Theoretically, this test could still fail.
|
- This is a flaky test. Theoretically, this test could still fail.
|
||||||
* This example is simple enough, but `function` might be doing some
|
- This example is simple enough, but `function` might be doing some
|
||||||
computationally expensive task, which would make this test severely
|
computationally expensive task, which would make this test severely
|
||||||
inefficient.
|
inefficient.
|
||||||
* The test is quite difficult to understand.
|
- The test is quite difficult to understand.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
|
|
||||||
@ -252,8 +247,7 @@ def test_function(mock_random):
|
|||||||
|
|
||||||
This is a deterministic test that clearly tells what's going on.
|
This is a deterministic test that clearly tells what's going on.
|
||||||
|
|
||||||
Unbalanced boilerplate
|
## Unbalanced boilerplate
|
||||||
----------------------
|
|
||||||
|
|
||||||
One thing to strive for in libraries is have as little boilerplate as possible,
|
One thing to strive for in libraries is have as little boilerplate as possible,
|
||||||
but not less.
|
but not less.
|
||||||
@ -269,8 +263,7 @@ library.
|
|||||||
|
|
||||||
I think Flask and SQLAlchemy do a very good job at keeping this under control.
|
I think Flask and SQLAlchemy do a very good job at keeping this under control.
|
||||||
|
|
||||||
Inconsistent use of verbs in functions
|
## Inconsistent use of verbs in functions
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -299,9 +292,9 @@ of toasts in the third function.
|
|||||||
|
|
||||||
This is based on personal taste but I have the following rule:
|
This is based on personal taste but I have the following rule:
|
||||||
|
|
||||||
* `get` prefixes function that return at most one object (they either return
|
- `get` prefixes function that return at most one object (they either return
|
||||||
none or raise an exception depending on the cases)
|
none or raise an exception depending on the cases)
|
||||||
* `find` prefixes function that return a possibly empty list (or iterable) of
|
- `find` prefixes function that return a possibly empty list (or iterable) of
|
||||||
objects.
|
objects.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
@ -324,8 +317,7 @@ def find_toasts(color):
|
|||||||
return filter(lambda toast: toast.color == color, TOASTS)
|
return filter(lambda toast: toast.color == color, TOASTS)
|
||||||
```
|
```
|
||||||
|
|
||||||
Opaque function arguments
|
## Opaque function arguments
|
||||||
-------------------------
|
|
||||||
|
|
||||||
A few variants of what I consider code that is difficult to debug:
|
A few variants of what I consider code that is difficult to debug:
|
||||||
|
|
||||||
@ -348,12 +340,11 @@ def create2(*args, **kwargs):
|
|||||||
|
|
||||||
Why is this bad?
|
Why is this bad?
|
||||||
|
|
||||||
* It's really easy to make a mistake, especially in interpreted languages such
|
- It's really easy to make a mistake, especially in interpreted languages such
|
||||||
as Python. For instance, if I call `create({'name': 'hello', 'ccolor':
|
as Python. For instance, if I call `create({'name': 'hello', 'ccolor': 'blue'})`, I won't get any error, but the color won't be the one I expect.
|
||||||
'blue'})`, I won't get any error, but the color won't be the one I expect.
|
- It increases cognitive load, as I have to understand where the object is
|
||||||
* It increases cognitive load, as I have to understand where the object is
|
|
||||||
coming from to introspect its content.
|
coming from to introspect its content.
|
||||||
* It makes the job of static analyzer harder or impossible.
|
- It makes the job of static analyzer harder or impossible.
|
||||||
|
|
||||||
Granted, this pattern is sometimes required (for instance when the number of
|
Granted, this pattern is sometimes required (for instance when the number of
|
||||||
params is too large, or when dealing with pure data).
|
params is too large, or when dealing with pure data).
|
||||||
@ -365,8 +356,7 @@ def create(name, color='red'):
|
|||||||
pass # ...
|
pass # ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Hiding formatting
|
## Hiding formatting
|
||||||
-----------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -401,8 +391,8 @@ def get_user(user_id):
|
|||||||
|
|
||||||
Even if you were duplicating the logic once or twice it might still be fine, because:
|
Even if you were duplicating the logic once or twice it might still be fine, because:
|
||||||
|
|
||||||
* You're unlikely to re-use anywhere else outside this file.
|
- You're unlikely to re-use anywhere else outside this file.
|
||||||
* Putting this inline makes it easier for follow the flow. Code is written to be read primarily by computers.
|
- Putting this inline makes it easier for follow the flow. Code is written to be read primarily by computers.
|
||||||
|
|
||||||
## Returning nothing instead of raising NotFound exception
|
## Returning nothing instead of raising NotFound exception
|
||||||
|
|
||||||
@ -456,8 +446,8 @@ def do_stuff_b(toaster):
|
|||||||
|
|
||||||
What's the correct things to do?
|
What's the correct things to do?
|
||||||
|
|
||||||
* If you expect the object to be there, make sure to raise if you don't find it.
|
- If you expect the object to be there, make sure to raise if you don't find it.
|
||||||
* If you're using SQLAlchemy, use `one()` to force raising an exception if the object can't be found. Don't use `first` or `one_or_none()`.
|
- If you're using SQLAlchemy, use `one()` to force raising an exception if the object can't be found. Don't use `first` or `one_or_none()`.
|
||||||
|
|
||||||
## Having a library that contains all utils
|
## Having a library that contains all utils
|
||||||
|
|
||||||
@ -476,6 +466,6 @@ def upload_to_sftp(...):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
`util` or `tools` or `lib` modules that contain all sorts of utilities have a tendency to become bloated and unmaintainable. Prefer to have small, dedicated files.
|
`util` or `tools` or `lib` modules that contain all sorts of utilities have a tendency to become bloated and unmaintainable. Prefer to have small, dedicated files.
|
||||||
|
|
||||||
This will keep your imports logical (`lib.date_utils`, `lib.csv_utils`, `lib.sftp`), make it easier for the reader to identify all the utilities around a specific topic, and test files easy to keep organized.
|
This will keep your imports logical (`lib.date_utils`, `lib.csv_utils`, `lib.sftp`), make it easier for the reader to identify all the utilities around a specific topic, and test files easy to keep organized.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Database anti-patterns](#database-anti-patterns)
|
- [Database anti-patterns](#database-anti-patterns)
|
||||||
@ -7,11 +8,9 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Database anti-patterns
|
# Database anti-patterns
|
||||||
======================
|
|
||||||
|
|
||||||
Using `VARCHAR` instead of `TEXT` (PostgreSQL)
|
## Using `VARCHAR` instead of `TEXT` (PostgreSQL)
|
||||||
----------------------------------------------
|
|
||||||
|
|
||||||
Unless you absolutely restrict the width of a text column for data consistency
|
Unless you absolutely restrict the width of a text column for data consistency
|
||||||
reason, don't do it.
|
reason, don't do it.
|
||||||
@ -22,9 +21,9 @@ shows that there's fundamentally no difference in performance between
|
|||||||
`char(n)`, `varchar(n)`, `varchar` and `text`. Here's why you should pick
|
`char(n)`, `varchar(n)`, `varchar` and `text`. Here's why you should pick
|
||||||
`text`:
|
`text`:
|
||||||
|
|
||||||
* `char(n)`: takes more space than necessary when dealing with values shorter
|
- `char(n)`: takes more space than necessary when dealing with values shorter
|
||||||
than n.
|
than n.
|
||||||
* `varchar(n)`: it's difficult to change the width.
|
- `varchar(n)`: it's difficult to change the width.
|
||||||
* `varchar` is just like `text`.
|
- `varchar` is just like `text`.
|
||||||
* `text` does not have the width problem that `char(n)` and `varchar(n)` and
|
- `text` does not have the width problem that `char(n)` and `varchar(n)` and
|
||||||
has a cleaner name than `varchar`.
|
has a cleaner name than `varchar`.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Error handling anti-patterns](#error-handling-anti-patterns)
|
- [Error handling anti-patterns](#error-handling-anti-patterns)
|
||||||
@ -12,8 +13,7 @@
|
|||||||
|
|
||||||
# Error handling anti-patterns
|
# Error handling anti-patterns
|
||||||
|
|
||||||
Hiding exceptions
|
## Hiding exceptions
|
||||||
-----------------
|
|
||||||
|
|
||||||
There are multiple variations of this anti-pattern:
|
There are multiple variations of this anti-pattern:
|
||||||
|
|
||||||
@ -40,9 +40,9 @@ def toast(bread):
|
|||||||
|
|
||||||
It depends on the context but in most cases this is a bad pattern:
|
It depends on the context but in most cases this is a bad pattern:
|
||||||
|
|
||||||
* **Debugging** those silent errors will be really difficult, because they won't show up in logs and exception reporting tool such as Sentry. Say you have an undefined variable in `Toaster.insert()`: it will raise `NameError`, which will be caught, and ignored, and you will never know about this developer error.
|
- **Debugging** those silent errors will be really difficult, because they won't show up in logs and exception reporting tool such as Sentry. Say you have an undefined variable in `Toaster.insert()`: it will raise `NameError`, which will be caught, and ignored, and you will never know about this developer error.
|
||||||
* **The user experience** will randomly degrade without anybody knowing about it, including the user.
|
- **The user experience** will randomly degrade without anybody knowing about it, including the user.
|
||||||
* **Identifying** those errors will be impossible. Say `do_stuff` does an HTTP request to another service, and that service starts misbehaving. There won't be any exception, any metric that will let you identify it.
|
- **Identifying** those errors will be impossible. Say `do_stuff` does an HTTP request to another service, and that service starts misbehaving. There won't be any exception, any metric that will let you identify it.
|
||||||
|
|
||||||
An article even named this [the most diabolical Python antipattern](https://realpython.com/blog/python/the-most-diabolical-python-antipattern/).
|
An article even named this [the most diabolical Python antipattern](https://realpython.com/blog/python/the-most-diabolical-python-antipattern/).
|
||||||
|
|
||||||
@ -84,14 +84,14 @@ __main__.ToastException: Could not toast bread
|
|||||||
Sometime it's tempting to think that graceful degradation is about silencing
|
Sometime it's tempting to think that graceful degradation is about silencing
|
||||||
exception. It's not.
|
exception. It's not.
|
||||||
|
|
||||||
* Graceful degradation needs to happen at the **highest level** of the code, so that the user can get a very explicit error message (e.g. "we're having issues with X, please retry in a moment"). That requires knowing that there was an error, which you can't tell if you're silencing the exception.
|
- Graceful degradation needs to happen at the **highest level** of the code, so that the user can get a very explicit error message (e.g. "we're having issues with X, please retry in a moment"). That requires knowing that there was an error, which you can't tell if you're silencing the exception.
|
||||||
* You need to know when graceful degradation happens. You also need to be
|
- You need to know when graceful degradation happens. You also need to be
|
||||||
alerted if it happens too often. This requires adding monitoring (using
|
alerted if it happens too often. This requires adding monitoring (using
|
||||||
something like statsd) and logging (Python's `logger.exception` automatically
|
something like statsd) and logging (Python's `logger.exception` automatically
|
||||||
adds the exception stacktrace to the log message for instance). Silencing an
|
adds the exception stacktrace to the log message for instance). Silencing an
|
||||||
exception won't make the error go away: all things being equal, it's better
|
exception won't make the error go away: all things being equal, it's better
|
||||||
for something to break hard, than for an error to be silenced.
|
for something to break hard, than for an error to be silenced.
|
||||||
* It is tempting to confound silencing the exception and fixing the exception. Say you're getting sporadic timeouts from a service. You might thing: let's ignore those timeouts and just do something else, like return an empty response. But this is very different from (1) actually finding the root cause for those timeouts (e.g. maybe a specific edge cases impacting certain objects) (2) doing proper graceful degradation (e.g. asking users to retry later because the request failed).
|
- It is tempting to confound silencing the exception and fixing the exception. Say you're getting sporadic timeouts from a service. You might thing: let's ignore those timeouts and just do something else, like return an empty response. But this is very different from (1) actually finding the root cause for those timeouts (e.g. maybe a specific edge cases impacting certain objects) (2) doing proper graceful degradation (e.g. asking users to retry later because the request failed).
|
||||||
|
|
||||||
In other words, ask yourself: would it be a problem if every single action was failing? If you're silencing the error, how would you know it's happening for every single action?
|
In other words, ask yourself: would it be a problem if every single action was failing? If you're silencing the error, how would you know it's happening for every single action?
|
||||||
|
|
||||||
@ -171,8 +171,7 @@ def main():
|
|||||||
toast('brioche')
|
toast('brioche')
|
||||||
```
|
```
|
||||||
|
|
||||||
Raising unrelated/unspecific exception
|
## Raising unrelated/unspecific exception
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
Most languages have predefined exceptions, including Python. It is important to make sure that the right exception is raised from a semantic standpoint.
|
Most languages have predefined exceptions, including Python. It is important to make sure that the right exception is raised from a semantic standpoint.
|
||||||
|
|
||||||
@ -201,7 +200,6 @@ def validate(toast):
|
|||||||
|
|
||||||
`TypeError` is here perfectly meaningful, and clearly convey the context around the error.
|
`TypeError` is here perfectly meaningful, and clearly convey the context around the error.
|
||||||
|
|
||||||
|
|
||||||
## Unconstrained defensive programming
|
## Unconstrained defensive programming
|
||||||
|
|
||||||
While defensive programming can be a very good technique to make the code more resilient, it can seriously backfire when misused. This is a very similar anti-pattern to carelessly silencing exceptions (see about this anti-pattern in this document).
|
While defensive programming can be a very good technique to make the code more resilient, it can seriously backfire when misused. This is a very similar anti-pattern to carelessly silencing exceptions (see about this anti-pattern in this document).
|
||||||
@ -219,13 +217,12 @@ def get_user_name(user_id):
|
|||||||
|
|
||||||
While this may look like a very good example of defensive programming (we're returning `unknown` when we can't find the user), this can have terrible repercussions, very similar to the one we have when doing an unrestricted bare `try... except`:
|
While this may look like a very good example of defensive programming (we're returning `unknown` when we can't find the user), this can have terrible repercussions, very similar to the one we have when doing an unrestricted bare `try... except`:
|
||||||
|
|
||||||
* A new developer might not know about this magical convention, and assume that `get_user_name` is guaranteed to return a true user name.
|
- A new developer might not know about this magical convention, and assume that `get_user_name` is guaranteed to return a true user name.
|
||||||
* The external service that we're getting user name from might start failing, and returning 404. We would silently return 'unknown' as a user name for all users, which could have terrible repercussions.
|
- The external service that we're getting user name from might start failing, and returning 404. We would silently return 'unknown' as a user name for all users, which could have terrible repercussions.
|
||||||
|
|
||||||
A much cleaner way is to raise an exception on 404, and let the caller decide how it wants to handle users that are not found.
|
A much cleaner way is to raise an exception on 404, and let the caller decide how it wants to handle users that are not found.
|
||||||
|
|
||||||
Unnecessarily catching and re-raising exceptions
|
## Unnecessarily catching and re-raising exceptions
|
||||||
------------------------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -299,7 +296,7 @@ Another problem with this pattern is that you can consider it quite useless to d
|
|||||||
|
|
||||||
> Error handling, and recovery are best done at the outer layers of your code base. This is known as the end-to-end principle. The end-to-end principle argues that it is easier to handle failure at the far ends of a connection than anywhere in the middle. If you have any handling inside, you still have to do the final top level check. If every layer atop must handle errors, so why bother handling them on the inside?
|
> Error handling, and recovery are best done at the outer layers of your code base. This is known as the end-to-end principle. The end-to-end principle argues that it is easier to handle failure at the far ends of a connection than anywhere in the middle. If you have any handling inside, you still have to do the final top level check. If every layer atop must handle errors, so why bother handling them on the inside?
|
||||||
|
|
||||||
*[Write code that is easy to delete, not easy to extend](http://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to)*
|
_[Write code that is easy to delete, not easy to extend](http://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to)_
|
||||||
|
|
||||||
A better way:
|
A better way:
|
||||||
|
|
||||||
@ -328,4 +325,4 @@ def call_3():
|
|||||||
|
|
||||||
More resources:
|
More resources:
|
||||||
|
|
||||||
* [Hiding exceptions](https://github.com/charlax/antipatterns/blob/master/code-antipatterns.md#hiding-exceptions)) anti-pattern.
|
- [Hiding exceptions](https://github.com/charlax/antipatterns/blob/master/code-antipatterns.md#hiding-exceptions)) anti-pattern.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [MVCS Antipatterns](#mvcs-antipatterns)
|
- [MVCS Antipatterns](#mvcs-antipatterns)
|
||||||
@ -7,15 +8,13 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
MVCS Antipatterns
|
# MVCS Antipatterns
|
||||||
=================
|
|
||||||
|
|
||||||
In simple terms, Model-View-Controller-Services add a few more layers to the
|
In simple terms, Model-View-Controller-Services add a few more layers to the
|
||||||
MVC pattern. The main one is the service, which owns all the core business
|
MVC pattern. The main one is the service, which owns all the core business
|
||||||
logic and manipulate the repository layer.
|
logic and manipulate the repository layer.
|
||||||
|
|
||||||
Creating entities for association tables
|
## Creating entities for association tables
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
You'll often need association tables, for instance to set up a many to many
|
You'll often need association tables, for instance to set up a many to many
|
||||||
relationships between users and their toasters. Let's assume that a toaster can
|
relationships between users and their toasters. Let's assume that a toaster can
|
||||||
@ -51,18 +50,18 @@ Heart of Software. Pearson Education. Kindle Edition.
|
|||||||
Entities should model business processes, not persistence details
|
Entities should model business processes, not persistence details
|
||||||
([source](http://blog.sapiensworks.com/post/2013/05/13/7-Biggest-Pitfalls-When-Doing-Domain-Driven-Design.aspx/)).
|
([source](http://blog.sapiensworks.com/post/2013/05/13/7-Biggest-Pitfalls-When-Doing-Domain-Driven-Design.aspx/)).
|
||||||
|
|
||||||
* In that case, `UserToaster` does not map to anything the business is using.
|
- In that case, `UserToaster` does not map to anything the business is using.
|
||||||
Using plain English, somebody might ask about "what toasters does user
|
Using plain English, somebody might ask about "what toasters does user
|
||||||
A owns?" or "who owns toaster B and since when?" Nobody would ask "give me
|
A owns?" or "who owns toaster B and since when?" Nobody would ask "give me
|
||||||
the UserToaster for user A".
|
the UserToaster for user A".
|
||||||
* The association table can be considered an implementation detail that should
|
- The association table can be considered an implementation detail that should
|
||||||
not (in most cases) leak in the domain layer. All the code should be dealing
|
not (in most cases) leak in the domain layer. All the code should be dealing
|
||||||
with the simpler logic of "user having toasters", not UserToaster objects
|
with the simpler logic of "user having toasters", not UserToaster objects
|
||||||
being an association between a user and a toaster. This makes the code more
|
being an association between a user and a toaster. This makes the code more
|
||||||
intuitive and natural.
|
intuitive and natural.
|
||||||
* It will be easier to handle serializing a "user having toasters" than
|
- It will be easier to handle serializing a "user having toasters" than
|
||||||
serializing UserToaster association.
|
serializing UserToaster association.
|
||||||
* This will make it very easy to force the calling site to take care of some
|
- This will make it very easy to force the calling site to take care of some
|
||||||
business logic. For instance, you might be able to get all `UserToaster`, and
|
business logic. For instance, you might be able to get all `UserToaster`, and
|
||||||
then filter on whether they were bought. You might be tempted to do that by
|
then filter on whether they were bought. You might be tempted to do that by
|
||||||
going through the `UserToaster` object and filtering those that have
|
going through the `UserToaster` object and filtering those that have
|
||||||
@ -72,11 +71,11 @@ Entities should model business processes, not persistence details
|
|||||||
|
|
||||||
So in that case, I would recommend doing the following:
|
So in that case, I would recommend doing the following:
|
||||||
|
|
||||||
* Create a `User` and `Toaster` entity.
|
- Create a `User` and `Toaster` entity.
|
||||||
* Put the association properties on the entity that makes sense, for instance
|
- Put the association properties on the entity that makes sense, for instance
|
||||||
`owned_since` would be on `Toaster`, even though in the database it's stored
|
`owned_since` would be on `Toaster`, even though in the database it's stored
|
||||||
on the association table.
|
on the association table.
|
||||||
* If filtering on association properties is required, put this logic in
|
- If filtering on association properties is required, put this logic in
|
||||||
repositories. In plain English, you would for instance ask "give all the
|
repositories. In plain English, you would for instance ask "give all the
|
||||||
toasters user A owned in December?", you wouldn't ask "give be all the
|
toasters user A owned in December?", you wouldn't ask "give be all the
|
||||||
UserToaster for owned by user A in December".
|
UserToaster for owned by user A in December".
|
||||||
@ -109,7 +108,7 @@ unidirectional user->toasters.
|
|||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
|
|
||||||
* [7 Biggest Pitfalls When Doing Domain Driven
|
- [7 Biggest Pitfalls When Doing Domain Driven
|
||||||
Design](http://blog.sapiensworks.com/post/2013/05/13/7-Biggest-Pitfalls-When-Doing-Domain-Driven-Design.aspx/)
|
Design](http://blog.sapiensworks.com/post/2013/05/13/7-Biggest-Pitfalls-When-Doing-Domain-Driven-Design.aspx/)
|
||||||
* [Domain-Driven Design: Tackling Complexity in the Heart of
|
- [Domain-Driven Design: Tackling Complexity in the Heart of
|
||||||
Software](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215)
|
Software](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Python Antipatterns](#python-antipatterns)
|
- [Python Antipatterns](#python-antipatterns)
|
||||||
@ -16,11 +17,9 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Python Antipatterns
|
# Python Antipatterns
|
||||||
===================
|
|
||||||
|
|
||||||
Redundant type checking
|
## Redundant type checking
|
||||||
-----------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -33,8 +32,7 @@ def toast(bread):
|
|||||||
```
|
```
|
||||||
|
|
||||||
In this case, checking against `None` is totally useless because in the next
|
In this case, checking against `None` is totally useless because in the next
|
||||||
line, `bread.is_toastable` would raise `AttributeError: 'NoneType' object has
|
line, `bread.is_toastable` would raise `AttributeError: 'NoneType' object has no attribute 'is_toastable'`. This is not a general rule, but in this case
|
||||||
no attribute 'is_toastable'`. This is not a general rule, but in this case
|
|
||||||
I would definitely argue that adding the type checks hurts readability and adds
|
I would definitely argue that adding the type checks hurts readability and adds
|
||||||
very little value to the function.
|
very little value to the function.
|
||||||
|
|
||||||
@ -46,15 +44,14 @@ def toast(bread):
|
|||||||
toaster.toast(bread)
|
toaster.toast(bread)
|
||||||
```
|
```
|
||||||
|
|
||||||
Restricting version in setup.py dependencies
|
## Restricting version in setup.py dependencies
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
Read those articles first:
|
Read those articles first:
|
||||||
|
|
||||||
* [setup.py vs.
|
- [setup.py vs.
|
||||||
requirements.txt](https://caremad.io/2013/07/setup-vs-requirement/)
|
requirements.txt](https://caremad.io/2013/07/setup-vs-requirement/)
|
||||||
* [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
- [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
||||||
* [Better Package Management](http://nvie.com/posts/better-package-management/)
|
- [Better Package Management](http://nvie.com/posts/better-package-management/)
|
||||||
|
|
||||||
**Summary: The main point is that `setup.py` should not specify explicit version
|
**Summary: The main point is that `setup.py` should not specify explicit version
|
||||||
requirements (good: `flask`, bad: `flask==1.1.1`).**
|
requirements (good: `flask`, bad: `flask==1.1.1`).**
|
||||||
@ -65,9 +62,9 @@ them both in application `app`.
|
|||||||
|
|
||||||
Yet in 99.999% of the cases, you don't need a specific version of flask, so:
|
Yet in 99.999% of the cases, you don't need a specific version of flask, so:
|
||||||
|
|
||||||
* `lib1` should just require `flask` in `setup.py` (no version specified, not
|
- `lib1` should just require `flask` in `setup.py` (no version specified, not
|
||||||
even with inequality operators: `flask<=2` is bad for instance)
|
even with inequality operators: `flask<=2` is bad for instance)
|
||||||
* `lib2` should just require `flask` in `setup.py` (same)
|
- `lib2` should just require `flask` in `setup.py` (same)
|
||||||
|
|
||||||
`app` will be happy using `lib1` and `lib2` with whatever version of `flask` it
|
`app` will be happy using `lib1` and `lib2` with whatever version of `flask` it
|
||||||
wants.
|
wants.
|
||||||
@ -77,8 +74,7 @@ strictly pinning (`==`) every single dependency. This way the app's stability
|
|||||||
will be very predictable, because always the same packages version will be
|
will be very predictable, because always the same packages version will be
|
||||||
installed.
|
installed.
|
||||||
|
|
||||||
Usually apps only use `requirements.txt`, not `setup.py`, because `pip install
|
Usually apps only use `requirements.txt`, not `setup.py`, because `pip install -r requirements.txt` is used when deploying.
|
||||||
-r requirements.txt` is used when deploying.
|
|
||||||
|
|
||||||
The only exception for pinning a dependency in a library is in case of a known
|
The only exception for pinning a dependency in a library is in case of a known
|
||||||
incompatibility, but again this should be a very temporary move, because that
|
incompatibility, but again this should be a very temporary move, because that
|
||||||
@ -87,8 +83,7 @@ will prevent people from upgrading.
|
|||||||
Ruby has a pretty similar dichotomy with [Gemspec and
|
Ruby has a pretty similar dichotomy with [Gemspec and
|
||||||
gemfile](http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/).
|
gemfile](http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/).
|
||||||
|
|
||||||
Unwieldy if... else instead of dict
|
## Unwieldy if... else instead of dict
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -147,31 +142,26 @@ def get_operator(value):
|
|||||||
raise ValueError('Unknown operator %s' % value)
|
raise ValueError('Unknown operator %s' % value)
|
||||||
```
|
```
|
||||||
|
|
||||||
Overreliance on kwargs
|
## Overreliance on kwargs
|
||||||
----------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Overreliance on list/dict comprehensions
|
## Overreliance on list/dict comprehensions
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Mutable default arguments
|
## Mutable default arguments
|
||||||
-------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Using `is` to compare objects
|
## Using `is` to compare objects
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
[Why you should almost never use “is” in
|
[Why you should almost never use “is” in
|
||||||
Python](http://blog.lerner.co.il/why-you-should-almost-never-use-is-in-python/)
|
Python](http://blog.lerner.co.il/why-you-should-almost-never-use-is-in-python/)
|
||||||
|
|
||||||
Instantiating exception with a dict
|
## Instantiating exception with a dict
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@ -246,14 +236,14 @@ The proper way to update a package and its dependency is to use another tool, fo
|
|||||||
|
|
||||||
**Reference**
|
**Reference**
|
||||||
|
|
||||||
* [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
- [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
||||||
|
|
||||||
# Reference
|
# Reference
|
||||||
|
|
||||||
* [Pythonic Pitfalls](http://nafiulis.me/potential-pythonic-pitfalls.html)
|
- [Pythonic Pitfalls](http://nafiulis.me/potential-pythonic-pitfalls.html)
|
||||||
* [Python Patterns](https://github.com/faif/python-patterns)
|
- [Python Patterns](https://github.com/faif/python-patterns)
|
||||||
* [The Little Book of Python
|
- [The Little Book of Python
|
||||||
Anti-Patterns](http://docs.quantifiedcode.com/python-anti-patterns/)
|
Anti-Patterns](http://docs.quantifiedcode.com/python-anti-patterns/)
|
||||||
* [How to make mistakes in
|
- [How to make mistakes in
|
||||||
Python](http://www.oreilly.com/programming/free/files/how-to-make-mistakes-in-python.pdf)
|
Python](http://www.oreilly.com/programming/free/files/how-to-make-mistakes-in-python.pdf)
|
||||||
G
|
G
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Not paging a database query](#not-paging-a-database-query)
|
- [Not paging a database query](#not-paging-a-database-query)
|
||||||
@ -7,12 +8,10 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Not paging a database query
|
## Not paging a database query
|
||||||
---------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Returning whole objects where primary key would suffice
|
## Returning whole objects where primary key would suffice
|
||||||
-------------------------------------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [SQLAlchemy Anti-Patterns](#sqlalchemy-anti-patterns)
|
- [SQLAlchemy Anti-Patterns](#sqlalchemy-anti-patterns)
|
||||||
@ -12,14 +13,12 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
SQLAlchemy Anti-Patterns
|
# SQLAlchemy Anti-Patterns
|
||||||
========================
|
|
||||||
|
|
||||||
This is a list of what I consider [SQLAlchemy](http://www.sqlalchemy.org/)
|
This is a list of what I consider [SQLAlchemy](http://www.sqlalchemy.org/)
|
||||||
anti-patterns.
|
anti-patterns.
|
||||||
|
|
||||||
Abusing lazily loaded relationships
|
## Abusing lazily loaded relationships
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -34,10 +33,10 @@ class Customer(Base):
|
|||||||
|
|
||||||
This suffers from severe performance inefficiencies:
|
This suffers from severe performance inefficiencies:
|
||||||
|
|
||||||
* The toaster will be loaded, as well as its toast. This involves creating and
|
- The toaster will be loaded, as well as its toast. This involves creating and
|
||||||
issuing the SQL query, waiting for the database to return, and instantiating
|
issuing the SQL query, waiting for the database to return, and instantiating
|
||||||
all those objects.
|
all those objects.
|
||||||
* `has_valid_toast` does not actually care about those objects. It just returns
|
- `has_valid_toast` does not actually care about those objects. It just returns
|
||||||
a boolean.
|
a boolean.
|
||||||
|
|
||||||
A better way would be to issue a SQL `EXISTS` query so that the database
|
A better way would be to issue a SQL `EXISTS` query so that the database
|
||||||
@ -61,8 +60,7 @@ class Customer(Base):
|
|||||||
This query might not always be the fastest if those relationships are small,
|
This query might not always be the fastest if those relationships are small,
|
||||||
and eagerly loaded.
|
and eagerly loaded.
|
||||||
|
|
||||||
Explicit session passing
|
## Explicit session passing
|
||||||
------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
@ -73,13 +71,11 @@ def toaster_exists(toaster_id, session):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Implicit transaction handling
|
## Implicit transaction handling
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Loading the full object when checking for object existence
|
## Loading the full object when checking for object existence
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -90,8 +86,8 @@ def toaster_exists(toaster_id):
|
|||||||
|
|
||||||
This is inefficient because it:
|
This is inefficient because it:
|
||||||
|
|
||||||
* Queries all the columns from the database (including any eagerly loaded joins)
|
- Queries all the columns from the database (including any eagerly loaded joins)
|
||||||
* Instantiates and maps all data on the Toaster model
|
- Instantiates and maps all data on the Toaster model
|
||||||
|
|
||||||
The database query would look something like this. You can see that all columns
|
The database query would look something like this. You can see that all columns
|
||||||
are selected to be loaded by the ORM.
|
are selected to be loaded by the ORM.
|
||||||
@ -123,8 +119,7 @@ FROM toasters
|
|||||||
WHERE toasters.id = 1) AS anon_1
|
WHERE toasters.id = 1) AS anon_1
|
||||||
```
|
```
|
||||||
|
|
||||||
Using identity as comparator
|
## Using identity as comparator
|
||||||
----------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -169,8 +164,8 @@ toasters = session.query(Toaster).filter(Toaster.deleted_at.is_(None)).all()
|
|||||||
```
|
```
|
||||||
|
|
||||||
See docs for
|
See docs for
|
||||||
[is_](http://docs.sqlalchemy.org/en/rel_1_0/core/sqlelement.html#sqlalchemy.sql.operators.ColumnOperators.is_).
|
[is\_](http://docs.sqlalchemy.org/en/rel_1_0/core/sqlelement.html#sqlalchemy.sql.operators.ColumnOperators.is_).
|
||||||
|
|
||||||
## Returning `None` instead of raising a `NoResultFound` exception
|
## Returning `None` instead of raising a `NoResultFound` exception
|
||||||
|
|
||||||
See [Returning nothing instead of raising NotFound exception](https://github.com/charlax/antipatterns/blob/master/code-antipatterns.md#returning-nothing-instead-of-raising-notfound-exception).
|
See [Returning nothing instead of raising NotFound exception](https://github.com/charlax/antipatterns/blob/master/code-antipatterns.md#returning-nothing-instead-of-raising-notfound-exception).
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Test antipatterns](#test-antipatterns)
|
- [Test antipatterns](#test-antipatterns)
|
||||||
@ -15,31 +16,25 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Test antipatterns
|
# Test antipatterns
|
||||||
=================
|
|
||||||
|
|
||||||
Testing implementation
|
## Testing implementation
|
||||||
----------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Testing configuration
|
## Testing configuration
|
||||||
---------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Testing multiple things
|
## Testing multiple things
|
||||||
-----------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Repeating integration tests for minor variations
|
## Repeating integration tests for minor variations
|
||||||
------------------------------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Over-reliance on centralized fixtures
|
## Over-reliance on centralized fixtures
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -71,7 +66,7 @@ efficient part of the developer flow.
|
|||||||
|
|
||||||
Lastly, this separate the setup and running part of the tests. It makes it more
|
Lastly, this separate the setup and running part of the tests. It makes it more
|
||||||
difficult for a new engineer to understand what is specific about this test's
|
difficult for a new engineer to understand what is specific about this test's
|
||||||
setup without having to open the ``fixtures`` file.
|
setup without having to open the `fixtures` file.
|
||||||
|
|
||||||
Here's a more explicit way to do this. Most fixtures libraries allow you to
|
Here's a more explicit way to do this. Most fixtures libraries allow you to
|
||||||
override default parameters, so that you can make clear what setup is specific
|
override default parameters, so that you can make clear what setup is specific
|
||||||
@ -83,13 +78,11 @@ def test_stuff():
|
|||||||
toaster_with_color_blue.toast('brioche')
|
toaster_with_color_blue.toast('brioche')
|
||||||
```
|
```
|
||||||
|
|
||||||
Over-reliance on replaying external requests
|
## Over-reliance on replaying external requests
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
Inefficient query testing
|
## Inefficient query testing
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -125,9 +118,7 @@ to verify the behavior.
|
|||||||
One would also recommend to not do this kind of integration testing for queries
|
One would also recommend to not do this kind of integration testing for queries
|
||||||
going to the database, but sometimes it's a good tradeoff.
|
going to the database, but sometimes it's a good tradeoff.
|
||||||
|
|
||||||
|
## Assertions in loop
|
||||||
Assertions in loop
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Bad:
|
Bad:
|
||||||
|
|
||||||
@ -159,22 +150,22 @@ at our test trying to do too many things.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
*The [test pyramid](https://martinfowler.com/bliki/TestPyramid.html). Image courtesy of Martin Fowler.*
|
_The [test pyramid](https://martinfowler.com/bliki/TestPyramid.html). Image courtesy of Martin Fowler._
|
||||||
|
|
||||||
Building lots of automated comprehensive end-to-end tests was tried multiple time, and almost never worked.
|
Building lots of automated comprehensive end-to-end tests was tried multiple time, and almost never worked.
|
||||||
|
|
||||||
* End to end tests try to do too many things, are highly indeterministic and as a result very flakey.
|
- End to end tests try to do too many things, are highly indeterministic and as a result very flakey.
|
||||||
* Debugging end to end tests failure is painful and slow - this is usually where most of the time is wasted.
|
- Debugging end to end tests failure is painful and slow - this is usually where most of the time is wasted.
|
||||||
* Building and maintaining e2e tests is very costly.
|
- Building and maintaining e2e tests is very costly.
|
||||||
* The time to run e2e tests is much much longer than unit tests.
|
- The time to run e2e tests is much much longer than unit tests.
|
||||||
|
|
||||||
Focused testing (e.g. unit, component, etc) prior to roll out, and business monitoring with alerting are much more efficient.
|
Focused testing (e.g. unit, component, etc) prior to roll out, and business monitoring with alerting are much more efficient.
|
||||||
|
|
||||||
More reading on this topic:
|
More reading on this topic:
|
||||||
|
|
||||||
* [End-To-End Testing Considered Harmful](http://www.alwaysagileconsulting.com/articles/end-to-end-testing-considered-harmful/), Always Agile Consulting
|
- [End-To-End Testing Considered Harmful](http://www.alwaysagileconsulting.com/articles/end-to-end-testing-considered-harmful/), Always Agile Consulting
|
||||||
* [Just Say No to More End-to-End Tests](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html), Google Testing Blog
|
- [Just Say No to More End-to-End Tests](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html), Google Testing Blog
|
||||||
* [Testing Strategies in a Microservice Architecture](https://martinfowler.com/articles/microservice-testing/#testing-end-to-end-tips), section titled "Writing and maintaining end-to-end tests can be very difficult", Toby Clemson, MartinFowler.com
|
- [Testing Strategies in a Microservice Architecture](https://martinfowler.com/articles/microservice-testing/#testing-end-to-end-tips), section titled "Writing and maintaining end-to-end tests can be very difficult", Toby Clemson, MartinFowler.com
|
||||||
* [Introducing the software testing ice-cream cone (anti-pattern)](https://watirmelon.blog/2012/01/31/introducing-the-software-testing-ice-cream-cone/), Alister Scott
|
- [Introducing the software testing ice-cream cone (anti-pattern)](https://watirmelon.blog/2012/01/31/introducing-the-software-testing-ice-cream-cone/), Alister Scott
|
||||||
* [TestPyramid](https://martinfowler.com/bliki/TestPyramid.html), Martin Fowler
|
- [TestPyramid](https://martinfowler.com/bliki/TestPyramid.html), Martin Fowler
|
||||||
* [professional-programming's testing section](https://github.com/charlax/professional-programming#testing)
|
- [professional-programming's testing section](https://github.com/charlax/professional-programming#testing)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Why use feature flags?](#why-use-feature-flags)
|
- [Why use feature flags?](#why-use-feature-flags)
|
||||||
@ -8,31 +9,29 @@
|
|||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
Why use feature flags?
|
# Why use feature flags?
|
||||||
======================
|
|
||||||
|
|
||||||
* **Velocity**: coupled with a system to rapidly deploy configuration change,
|
- **Velocity**: coupled with a system to rapidly deploy configuration change,
|
||||||
it is usually much faster to disable new code by turning off a feature than
|
it is usually much faster to disable new code by turning off a feature than
|
||||||
re-deploying all the code. This extra safety net helps developers be more
|
re-deploying all the code. This extra safety net helps developers be more
|
||||||
confident in their release, because they know they can roll back a change
|
confident in their release, because they know they can roll back a change
|
||||||
very rapidly in case of error.
|
very rapidly in case of error.
|
||||||
* **Testing**: the presence of a feature flag forces the feature owner to test
|
- **Testing**: the presence of a feature flag forces the feature owner to test
|
||||||
the flow. Without feature flags, the developer might deploy the change and
|
the flow. Without feature flags, the developer might deploy the change and
|
||||||
assume the absence of errors means the release was successful. Yet there's
|
assume the absence of errors means the release was successful. Yet there's
|
||||||
numerous failure mode that don't raise explicit errors.
|
numerous failure mode that don't raise explicit errors.
|
||||||
* **Code iterations**: because code can be kept hidden behind a feature flag
|
- **Code iterations**: because code can be kept hidden behind a feature flag
|
||||||
until it's ready to go live, developers can push smaller code changes that
|
until it's ready to go live, developers can push smaller code changes that
|
||||||
are not fully integrated yet. Smaller pull requests ease the job of code
|
are not fully integrated yet. Smaller pull requests ease the job of code
|
||||||
reviewers, make testing easier, and reduce the probability of a catastrophic
|
reviewers, make testing easier, and reduce the probability of a catastrophic
|
||||||
failure.
|
failure.
|
||||||
* **Gradual rollout**: feature flags enable gradual rollout, where a piece of
|
- **Gradual rollout**: feature flags enable gradual rollout, where a piece of
|
||||||
code is gradually activated, for instance on a per city basis, or on a per
|
code is gradually activated, for instance on a per city basis, or on a per
|
||||||
user group basis. This builds confidence in the feature release process, and
|
user group basis. This builds confidence in the feature release process, and
|
||||||
allows the engineer to verify that the new implementation is actually better
|
allows the engineer to verify that the new implementation is actually better
|
||||||
(for instance, when coupled with A/B testing frameworks).
|
(for instance, when coupled with A/B testing frameworks).
|
||||||
|
|
||||||
Should feature flags be used for everything?
|
## Should feature flags be used for everything?
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
I don't think so. I think it's a matter of good judgment. Just like 100% test
|
I don't think so. I think it's a matter of good judgment. Just like 100% test
|
||||||
coverage does not always make sense (provided lines that are not tested are
|
coverage does not always make sense (provided lines that are not tested are
|
||||||
@ -41,24 +40,23 @@ decision.
|
|||||||
|
|
||||||
When would I not use a feature flag?
|
When would I not use a feature flag?
|
||||||
|
|
||||||
* Simple changes: copy, logging, etc.
|
- Simple changes: copy, logging, etc.
|
||||||
* When rolling back takes a few seconds
|
- When rolling back takes a few seconds
|
||||||
* Feature that is used only in asynchronous jobs that are safe to retry and
|
- Feature that is used only in asynchronous jobs that are safe to retry and
|
||||||
don't impact the user experience.
|
don't impact the user experience.
|
||||||
|
|
||||||
When should a feature flag be used?
|
When should a feature flag be used?
|
||||||
|
|
||||||
* Large refactors
|
- Large refactors
|
||||||
* Changing integration points
|
- Changing integration points
|
||||||
* Performance optimization
|
- Performance optimization
|
||||||
* New flows
|
- New flows
|
||||||
|
|
||||||
References
|
## References
|
||||||
----------
|
|
||||||
|
|
||||||
* Martin Fowler,
|
- Martin Fowler,
|
||||||
[FeatureToggle](http://martinfowler.com/bliki/FeatureToggle.html)
|
[FeatureToggle](http://martinfowler.com/bliki/FeatureToggle.html)
|
||||||
* Flickr, [Flipping Out](http://code.flickr.net/2009/12/02/flipping-out/): one
|
- Flickr, [Flipping Out](http://code.flickr.net/2009/12/02/flipping-out/): one
|
||||||
of the first articles on the topic.
|
of the first articles on the topic.
|
||||||
* [Using Feature Flags to Ship Changes with
|
- [Using Feature Flags to Ship Changes with
|
||||||
Confidence](http://blog.travis-ci.com/2014-03-04-use-feature-flags-to-ship-changes-with-confidence/)
|
Confidence](http://blog.travis-ci.com/2014-03-04-use-feature-flags-to-ship-changes-with-confidence/)
|
||||||
|
21
glossary.md
21
glossary.md
@ -1,23 +1,24 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Glossary](#glossary)
|
- [Glossary](#glossary)
|
||||||
- [Second system effect](#second-system-effect)
|
- [Second system effect](#second-system-effect)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
# Glossary
|
# Glossary
|
||||||
|
|
||||||
* **Blackbox monitoring**
|
- **Blackbox monitoring**
|
||||||
* **Conceptual integrity**: "It is better to have a system omit certain anomalous features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas." ([Fred Brooks](http://wiki.c2.com/?ConceptualIntegrity))
|
- **Conceptual integrity**: "It is better to have a system omit certain anomalous features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas." ([Fred Brooks](http://wiki.c2.com/?ConceptualIntegrity))
|
||||||
* **End-to-end principle**: "application-specific features reside in the communicating end nodes of the network, rather than in intermediary nodes" ([end-to-end principle - Wikipedia](https://en.wikipedia.org/wiki/End-to-end_principle))
|
- **End-to-end principle**: "application-specific features reside in the communicating end nodes of the network, rather than in intermediary nodes" ([end-to-end principle - Wikipedia](https://en.wikipedia.org/wiki/End-to-end_principle))
|
||||||
* **[NIHITO](http://pragmaticmarketing.com/resources/use-the-market-to-gain-credibility)** (Nothing Important Happens In The Office): you need to learn from your customers and from the market first.
|
- **[NIHITO](http://pragmaticmarketing.com/resources/use-the-market-to-gain-credibility)** (Nothing Important Happens In The Office): you need to learn from your customers and from the market first.
|
||||||
* **Stability** is the sensitivity to change of a given system that is the negative impact that may be caused by system changes ([ISO/IEC 9126 standard on evaluating software quality](https://en.wikipedia.org/wiki/ISO/IEC_9126)).
|
- **Stability** is the sensitivity to change of a given system that is the negative impact that may be caused by system changes ([ISO/IEC 9126 standard on evaluating software quality](https://en.wikipedia.org/wiki/ISO/IEC_9126)).
|
||||||
* **Reliability** is made of maturity (frequency of failure in software), fault tolerance (ability to withstand failure) and recoverability (ability to bring back a failed system to full operation) ([ISO/IEC 9126 standard on evaluating software quality](https://en.wikipedia.org/wiki/ISO/IEC_9126)).
|
- **Reliability** is made of maturity (frequency of failure in software), fault tolerance (ability to withstand failure) and recoverability (ability to bring back a failed system to full operation) ([ISO/IEC 9126 standard on evaluating software quality](https://en.wikipedia.org/wiki/ISO/IEC_9126)).
|
||||||
* **Two Generals' Problem**: "a thought experiment meant to illustrate the pitfalls and design challenges of attempting to coordinate an action by communicating over an unreliable link" ([wikipedia](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)).
|
- **Two Generals' Problem**: "a thought experiment meant to illustrate the pitfalls and design challenges of attempting to coordinate an action by communicating over an unreliable link" ([wikipedia](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)).
|
||||||
* **Whitebox monitoring**
|
- **Whitebox monitoring**
|
||||||
* **Law of Demeter**: only talk to your immediate friends ([wikipedia](https://en.wikipedia.org/wiki/Law_of_Demeter))
|
- **Law of Demeter**: only talk to your immediate friends ([wikipedia](https://en.wikipedia.org/wiki/Law_of_Demeter))
|
||||||
|
|
||||||
### Second system effect
|
### Second system effect
|
||||||
|
|
||||||
|
@ -17,11 +17,11 @@ console.log("hello");
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ???
|
// ???
|
||||||
console.assert('1' == 1)
|
console.assert("1" == 1);
|
||||||
|
|
||||||
// Better
|
// Better
|
||||||
console.assert(!('1' === 1))
|
console.assert(!("1" === 1));
|
||||||
console.assert('1' !== 1)
|
console.assert("1" !== 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Comparing non-scalar
|
#### Comparing non-scalar
|
||||||
@ -29,22 +29,22 @@ console.assert('1' !== 1)
|
|||||||
Applied on arrays and objects, `==` and `===` will check for object identity, which is almost never what you want.
|
Applied on arrays and objects, `==` and `===` will check for object identity, which is almost never what you want.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
console.assert({a: 1} != {a: 1})
|
console.assert({ a: 1 } != { a: 1 });
|
||||||
console.assert({a: 1} !== {a: 1})
|
console.assert({ a: 1 } !== { a: 1 });
|
||||||
|
|
||||||
const obj = {a: 1}
|
const obj = { a: 1 };
|
||||||
const obj2 = obj
|
const obj2 = obj;
|
||||||
console.assert(obj == obj2)
|
console.assert(obj == obj2);
|
||||||
console.assert(obj === obj2)
|
console.assert(obj === obj2);
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a library such as [lodash](https://lodash.com/) to properly compare objects and array
|
Use a library such as [lodash](https://lodash.com/) to properly compare objects and array
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import _ from 'lodash'
|
import _ from "lodash";
|
||||||
|
|
||||||
console.assert(_.isEqual({a: 1}, {a: 1}))
|
console.assert(_.isEqual({ a: 1 }, { a: 1 }));
|
||||||
console.assert(_.isEqual([1, 2], [1, 2]))
|
console.assert(_.isEqual([1, 2], [1, 2]));
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Object` methods
|
### `Object` methods
|
||||||
@ -62,34 +62,34 @@ console.assert(_.isEqual([1, 2], [1, 2]))
|
|||||||
### Objects
|
### Objects
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const toaster = {size: 2, color: 'red', brand: 'NoName'};
|
const toaster = { size: 2, color: "red", brand: "NoName" };
|
||||||
|
|
||||||
// Get one object key
|
// Get one object key
|
||||||
const {size} = toaster;
|
const { size } = toaster;
|
||||||
console.assert(size === 2)
|
console.assert(size === 2);
|
||||||
|
|
||||||
// Get the rest with ...rest
|
// Get the rest with ...rest
|
||||||
const {color, brand, ...rest} = toaster;
|
const { color, brand, ...rest } = toaster;
|
||||||
console.assert(_.isEqual(rest, {size: 2}));
|
console.assert(_.isEqual(rest, { size: 2 }));
|
||||||
|
|
||||||
// Set default
|
// Set default
|
||||||
const {size2 = 3} = toaster
|
const { size2 = 3 } = toaster;
|
||||||
console.assert(size2 === 3)
|
console.assert(size2 === 3);
|
||||||
|
|
||||||
// Rename variables
|
// Rename variables
|
||||||
const {size: size3} = toaster
|
const { size: size3 } = toaster;
|
||||||
console.assert(size3 === 2)
|
console.assert(size3 === 2);
|
||||||
|
|
||||||
// Enhances object literals
|
// Enhances object literals
|
||||||
const name = 'Louis'
|
const name = "Louis";
|
||||||
const person = {name}
|
const person = { name };
|
||||||
console.assert(_.isEqual(person, {name: 'Louis'}))
|
console.assert(_.isEqual(person, { name: "Louis" }));
|
||||||
|
|
||||||
// Dynamic properties
|
// Dynamic properties
|
||||||
const person2 = {['first' + 'Name']: 'Olympe'}
|
const person2 = { ["first" + "Name"]: "Olympe" };
|
||||||
console.assert(_.isEqual(person2, {firstName: 'Olympe'}))
|
console.assert(_.isEqual(person2, { firstName: "Olympe" }));
|
||||||
// Btw, you can include quotes although nobody does this
|
// Btw, you can include quotes although nobody does this
|
||||||
console.assert(_.isEqual(person2, {'firstName': 'Olympe'}))
|
console.assert(_.isEqual(person2, { firstName: "Olympe" }));
|
||||||
```
|
```
|
||||||
|
|
||||||
### Array
|
### Array
|
||||||
@ -107,29 +107,29 @@ console.assert(_.isEqualWith(rest, [3]));
|
|||||||
## `let` and `const`
|
## `let` and `const`
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const constantVar = 'a';
|
const constantVar = "a";
|
||||||
|
|
||||||
// Raises "constantVar" is read-only
|
// Raises "constantVar" is read-only
|
||||||
constantVar = 'b';
|
constantVar = "b";
|
||||||
|
|
||||||
let mutableVar = 'a';
|
let mutableVar = "a";
|
||||||
mutableVar = 'a';
|
mutableVar = "a";
|
||||||
|
|
||||||
// Note: this will work ok
|
// Note: this will work ok
|
||||||
const constantObject = {a: 1}
|
const constantObject = { a: 1 };
|
||||||
constantObject.a = 2
|
constantObject.a = 2;
|
||||||
constantObject.b = 3
|
constantObject.b = 3;
|
||||||
|
|
||||||
// Raises: "constantObject" is read-only
|
// Raises: "constantObject" is read-only
|
||||||
constantObject = {a: 1}
|
constantObject = { a: 1 };
|
||||||
|
|
||||||
// const and let are block scoped. A block is enclosed in {}
|
// const and let are block scoped. A block is enclosed in {}
|
||||||
{
|
{
|
||||||
const a = 'a';
|
const a = "a";
|
||||||
console.log({a})
|
console.log({ a });
|
||||||
}
|
}
|
||||||
// Raises: ReferenceError: a is not defined
|
// Raises: ReferenceError: a is not defined
|
||||||
console.log({a})
|
console.log({ a });
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: try to use `const` as much as you can.
|
Note: try to use `const` as much as you can.
|
||||||
@ -152,29 +152,29 @@ The first advantage of arrow function is that they're shorter to write:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// You can define a function this way:
|
// You can define a function this way:
|
||||||
const myFunction = function() {
|
const myFunction = function () {
|
||||||
console.log("hello world");
|
console.log("hello world");
|
||||||
}
|
};
|
||||||
|
|
||||||
// With an arrow function, you save a few characters:
|
// With an arrow function, you save a few characters:
|
||||||
const myArrowFunction = () => {
|
const myArrowFunction = () => {
|
||||||
console.log("hello world");
|
console.log("hello world");
|
||||||
}
|
};
|
||||||
|
|
||||||
// Some things, like params parentheses, and function code brackets, are optional
|
// Some things, like params parentheses, and function code brackets, are optional
|
||||||
const myFunctionToBeShortened = function(a) {
|
const myFunctionToBeShortened = function (a) {
|
||||||
return a;
|
return a;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Shorter arrow function
|
// Shorter arrow function
|
||||||
const myFunctionToBeShortenedArrowV1 = (a) => {
|
const myFunctionToBeShortenedArrowV1 = (a) => {
|
||||||
return a;
|
return a;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Shortest arrow function
|
// Shortest arrow function
|
||||||
// Remove single param parenthesis, remove function code bracket, remove return
|
// Remove single param parenthesis, remove function code bracket, remove return
|
||||||
const myFunctionToBeShortenedArrowV2 = a => a
|
const myFunctionToBeShortenedArrowV2 = (a) => a;
|
||||||
console.assert(myFunctionToBeShortenedArrowV2(1) === 1)
|
console.assert(myFunctionToBeShortenedArrowV2(1) === 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
### How `this` works in arrow functions
|
### How `this` works in arrow functions
|
||||||
|
Loading…
Reference in New Issue
Block a user