Coding guidelines for continuous development by the team. It is not intended for a specific language, but is mainly intended for statically typed object-oriented languages. Sample code is described in Swift unless otherwise specified.
--Increase productivity and reduce maintenance costs --It makes it difficult for bugs to occur —— Make the code easier for development members (especially new participants) to understand --Education of beginner programmers
The [: star: number] at the beginning of the title is the importance. The higher the value, the greater the effect on the system, but the lower the value, the smaller the effect and the easier it is to repair.
Variable values create complexity and lead to misunderstandings and bugs, so programs are less likely to have problems with fewer variables. As a general rule of programming, keep the minimum necessary variables and do not increase them unnecessarily.
Also, the larger the scope and lifespan of a variable, the greater the adverse effect, so when using a variable, try to minimize the scope and lifespan. Basically, the above ones have a wider scope, so try to avoid using them.
As mentioned above, the larger the scope of a variable, the greater the harm, so avoid using the global variable with the largest scope as much as possible.
Variables that can be read and written from anywhere are called global variables.
In addition to so-called global variables such as C language, static variables (class variables) such as Java are often called global variables. Furthermore, I will expand the meaning a little more here, and proceed with the discussion of singletons and shared objects that can be accessed from anywhere as global variables.
Also, storage such as databases and files has the same property of being readable and writable from anywhere, so although it is slightly irregular, it is treated as a kind of global variable here.
Since global variables can read and write values from anywhere, they are likely to cause the following problems.
In addition, the existence of global variables can cause classes and modules that are supposed to be unrelated to each other to influence each other through global variables (so-called tightly coupled state). As a result, it is difficult to reuse classes and modules, it is difficult to isolate problems, and it is difficult to perform unit tests.
So why not use global variables at all? That's not the case. As mentioned above, singletons, shared objects, and DBs are also defined as a type of global variable, but it is difficult to create an application without using them at all.
Applications often have context-like information that is commonly needed throughout, such as settings and session information. It is easier to implement by providing a method that can be accessed from anywhere, such as a global variable, rather than passing such information to another object like a bucket relay as a function argument.
However, the policy of avoiding the use of global variables as much as possible remains unchanged. Carefully consider the overall design and ensure that only the minimum necessary global access methods are available.
In web applications (PHP, Rails, SpringMVC, ASP, etc.) that generate HTML on the server side, DBs and sessions play the role of global variables, so in a strict sense, so-called global variables (PHP global variables and Java static variables, etc.) are rarely needed. If you are using so-called global variables in your web app, there is a high possibility that there is a design problem.
Also, it seems that short-term processes such as batch processing rarely require so-called global variables excluding DB.
On the other hand, in front-end applications such as smartphones, desktop applications, and SPAs, global variables are often prepared and used by themselves. In such a case, avoid the following usage.
On the flip side, global variables exist permanently rather than temporarily, are provided for information that is commonly used by various features of your application, and should be used if the classes you access are limited to some. It will be good.
When creating global variables and static variables, instead of holding simple data types such as Int and String in variables as they are, make related data objects and hold them in the form of singletons or shared objects. Is good.
Bad
var userName: String = ""
var loginPassword: String = ""
Good
class AccountInfo {
static var shared = AccountInfo()
var userName: String = ""
var loginPassword: String = ""
}
Also, if anyone can add such shared objects indefinitely, it will be out of control, so it is preferable that some experts in the team design the shared objects.
Furthermore, even if you create a global variable, you should not refer to it from various classes unnecessarily. Decide which layer you can access the global variable, and do not access it from other layers.
I wrote that global variables should be in the form of singletons or shared objects, but in reality, replaceable shared objects are better than singletons.
Bad
class Sample {
//Singleton that cannot be reassigned
static let shared = Sample()
}
Good
class Sample {
//Reassignable shared object
static var shared = Sample()
}
Since the singleton has only one instance, it has the following disadvantages. These disadvantages are especially obstacles for Unit Test and may be a problem for application implementation.
If it is a shared object, it can eliminate the above disadvantages while having a singleton-like function. Unlike singletons, shared objects are not mechanically guaranteed to have one instance, but if you want to have one instance, you can share such a design policy within the development team.
Related article [Don't lose the temptation of the singleton pattern](https://xn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/%E3 % 82% B7% E3% 83% B3% E3% 82% B0% E3% 83% AB% E3% 83% 88% E3% 83% B3% E3% 83% 91% E3% 82% BF% E3% 83 % BC% E3% 83% B3% E3% 81% AE% E8% AA% 98% E6% 83% 91% E3% 81% AB% E8% B2% A0% E3% 81% 91% E3% 81% AA % E3% 81% 84 /)
--Save related data together as an object --Save data that exists permanently rather than temporarily --Save data that is commonly used by various functions of the application --Limit some classes to access global variables -Designed by some experts in the team, not everyone is free to add
Looking back on the above-mentioned things written in the previous sections regarding global variables using shared objects, this is very similar to the positioning of DB. It is easy to imagine global variables if you design them as something like a repository or Repository that is spoken in MVC patterns.
On platforms with DI containers (such as Spring and Angular), it is better to manage shared objects with DI containers.
In an environment with a DI container, the entire application tends to be designed to depend on the DI container. Whether it is good or bad, it is easier to understand in such an environment that it is better to leave the management to the DI container instead of managing the shared objects by yourself.
If you pass all the necessary information as arguments like a bucket relay, you can create an application without using global variables at all. It is a form in which the necessary information is passed from the outside instead of being picked up by the user (hereinafter referred to as DI).
However, even in the form of DI, if you pass an object whose state can be changed, that object will be changed by multiple users and treated like a global variable, so the information passed in DI cannot change state ( Must only be non-writable) value objects.
By thoroughly passing immutable objects like a bucket relay like this, it seems that things like global variables can be completely eliminated, and at first glance it will be a loosely coupled and beautiful design, but this policy Actually, there are pitfalls.
The problem with this policy is that all the intermediary objects (passers of the bucket relay) must temporarily have the information needed by the objects at the end of the bucket relay. Since this problem requires a specific class to temporarily have irrelevant information, it cannot be said that it is loosely coupled.
After all, while avoiding the use of global variables, it is not realistic to use them at all, and it may be best to use global variables under the right policy.
Like global variables, avoid using instance variables as much as possible. When adding a new variable, think carefully about whether it is absolutely necessary rather than adding it casually. If there are two classes that implement the same function, it is important to say that the design is better with fewer instance variables.
I wrote that I should avoid using instance variables as much as possible, but when it comes to how to reduce variables, the following three are the basics.
As is often the case with beginners, don't just use instance variables to reuse data in multiple functions. If the data does not need to be stored for a long time, pass it as a function argument without using an instance variable.
bad
class Foo {
var user: User?
func setUser(user: User) {
self.user = user
printName()
printEmail()
}
func printName() {
print(user?.name)
}
func printEmail() {
print(user?.email)
}
}
good
class Foo {
func setUser(user: User) {
printName(user: user)
printEmail(user: user)
}
func printName(user: User) {
print(user.name)
}
func printEmail(user: User) {
print(user.email)
}
}
Related article Incorrect use of instance variables (which you may see around you)
It may be unavoidable for performance optimization, but basically it does not hold the processed value of some value in the instance variable.
In the following example, ʻitemsA and ʻitemsB
are the processed values of ʻitems`.
bad
class Foo {
var items = ["A-1", "A-2", "B-1", "B-2"]
let itemsA: [String]
let itemsB: [String]
init() {
itemsA = items.filter { $0.hasPrefix("A-") }
itemsB = items.filter { $0.hasPrefix("B-") }
}
}
In this example, the values of ʻitemsA and ʻitemsB
do not need to be instance variables, only ʻitems can be made a variable, and ʻitemsA
and ʻitemsB` can be generated from it by a function.
good
class Foo {
var items = ["A-1", "A-2", "B-1", "B-2"]
func itemsA() -> [String] {
return items.filter { $0.hasPrefix("A-") }
}
func itemsB() -> [String] {
return items.filter { $0.hasPrefix("B-") }
}
}
I get a little bit of "Do not hold the processed value in the instance variable", but the information that can be judged from other values and the information that can be obtained by some API or program are not saved in the instance variable, and the program is executed each time it is needed. Run and get.
It's a bit difficult for beginners, but closures can be used to reduce unnecessary instance variables.
Instance variables are basically used to hold data for a long time, but closures allow you to hold data for a long time without using instance variables.
In the following example, by using the after form, dataType
can be retained until API communication is completed without using instance variables.
before
class Before {
var dataType: DataType?
func fetchData(dataType: DataType) {
self.dataType = dataType //Save to instance variable
APIConnection(dataType).getData(onComplete: { response in
self.setResponse(response)
})
}
func setResponse(_ response: Response) {
print("\(dataType)Was set")
}
}
after
class After {
func fetchData(dataType: DataType) {
APIConnection(dataType).getData(onComplete: { response in
//By using datType in the closure, dataType can be retained until API communication is completed.
self.setResponse(response, dataType: dataType)
})
}
func setResponse(_ response: Response, dataType: DataType) {
print("\(dataType)Was set")
}
}
Functional programming can be used to reduce variables and narrow the scope of variables. In some functional languages such as Haskell, variables cannot be reassigned in the first place, so it can be said that there are no variables in a sense.
Even with functional programming, the data is eventually stored somewhere, but the scope and scope of influence are reduced, allowing you to write less harmful code.
You can use it, but try to minimize the scope by defining it only when you need it.
Bad
var num = 0
for i in list {
num = i
print(num)
}
Good
for i in list {
let num = i
print(num)
}
However, in some languages (such as dealing with C and JavaScript var windings), it may be necessary to declare variables at the beginning of the scope.
For readability, we may create variables that are not necessary for implementation and assign the result of the expression once.
Such variables are called explanatory variables
, and unlike other variables, they can be actively used if necessary.
There is no need to reassign the value to the explanatory variable, and it is treated like a constant, so increasing it does not cause much harm.
before
let totalPrice = ((orangePrice * orangeQuantity) + (applePrice * appleQuanitity)) * (1 + taxPercentage / 100)
after
//The following three are explanatory variables
let orangePriceSum = orangePrice * orangeQuantity
let applePriceSum = applePrice * appleQuanitity
let includesTaxRate = 1 + taxPercentage / 100
let totalPrice = (orangePriceSum + applePriceSum) * includesTaxRate
Explanatory variables increase the number of lines of code but improve readability, and have the advantage of making it easier to debug using breakpoints because you can see the results in the middle of an expression.
When storing some state in a variable, the lifetime of the value should be as short as possible. Save the value when it is needed and clear it as soon as possible when it is no longer needed. The value saved in the variable is a snapshot of that moment, and there is a risk that it will deviate from the latest state over time, so the life of the value saved in the variable should be as short as possible.
Do not retain the same information more than once. For example, in the following example, the age is stored in two fields in different forms, and the information is duplicated.
Bad
class Person {
var age = 17
var ageText = "17 years old"
}
In such a case, it is better to combine the information to be retained as shown below and process the value for use.
Good
class Person {
var age = 17
var ageText: String {
return "\(age)age"
}
}
Duplication of information has the following adverse effects on the system.
――I don't know which of the multiple information to use --If only a part of multiple information is updated, there will be differences and inconsistencies. --You may refer to or update incorrect information. --If you want to change the information, you need to change multiple fields
This example is a simple and easy-to-understand information duplication, but there are various other forms of information duplication.
In the following example, the data in the DB is read and held (cached) in the instance variable, but this causes the information held in the instance variable and the information in the DB to be duplicated.
Bad
class Foo {
var records: [DBRecord]?
func readDBRecord(dbTable: DBTable) {
records = dbTable.selectAllRecords()
}
}
It is better to avoid holding the data read from DB in the instance variable as described above.
If the Foo
object exists for a long period of time and the DB is updated during that time, there will be a difference between the instance variable information that Foo has and the DB information.
In the following example, 0, 1, and 2 are used for the Dictionary (Map) key, but since there is an index if it is an Array, it is unnecessary information if you want to take an order.
Bad
func getName(index: Int) -> String? {
let map = [0: "Sato", 1: "Shinagawa", 2: "Suzuki"]
return map[index]
}
If it is set to Array, it can be accessed by index, so 0 to 2 information is unnecessary.
Good
func getName2(index: Int) -> String? {
let names = ["Sato", "Shinagawa", "Suzuki"]
if 0 <= index && index < names.count {
return names[index]
}
return nil
}
The policy of not duplicating information is useful not only in programming but also in document management.
Bad
When I copied the specification to the local machine and looked at it, the specification was updated and I was coding based on the old specification.
There are cases where it is necessary to copy locally due to various circumstances, but in the above example, a problem occurs due to copying and duplication of specifications.
Good
There is only one spec on the shared server, so you can always see the latest specs.
The problem in this section is not to have duplicate information, but not to duplicate code with similar logic **. Don't get me wrong because the case of "copying the code to make multiple similar codes" is different from the duplication here. Sharing code with similar logic is another story, with a lower priority policy.
Conversely, avoid having multiple types of information in one field (variables, DB columns, text UI, etc.). Putting multiple types of information in one field complicates the implementation and poses the risk of creating various bugs.
If you make a mistake, don't reuse one variable for multiple purposes.
Give appropriate names to programmatically defined elements such as classes, properties, functions, and constants. In particular, the name of the class has a great influence on the design and is very important.
If class names, property names, and function names can be given appropriately, it is almost synonymous with class design, data design, and interface design. In a sense, it is no exaggeration to say that programming is the task of naming.
When giving a name, keep the following in mind.
――The purpose and meaning can be understood from the name even if others see it. --The name matches the actual use and processing --It has no processing or role other than what is written in the name.
Also, the wider the scope of functions and variables, the more polite the names should be.
Variables do nothing more than what is written in the name. The function does nothing but what is written in the name.
For example, in the case of a class called LoginViewController
, only the process of controlling the View of Login is described there, and other processes such as login authentication are not described in principle.
(However, if it is a short process, it may be described in the same file according to the rule of "Keep related items close")
It's just a matter of updating the state of something with a getter like getHoge ()
in Java.
Bad
var code1 = "a"
func func001() {}
enum VieID {
case vol_01, vol_02, vol_03
}
Avoid using numbers and IDs like the ones in your program for names, as strangers don't know what they mean. Also, if you use an ID in your code, you will need to modify the program if the ID changes.
Bad
func chkDispFlg(){}
As many of you may know, the above is an abbreviation for checkDisplayFlag
.
This abbreviation of words is a traditional culture of programmers, but in modern times there is almost no code completion by IDE, so there is not much merit in omitting it.
Do not omit words as it will be difficult for a third party to understand the meaning.
Bad
let yen = "Circle"
Avoid using the specific content of the value as it is because it is not extensible.
For example, if the above "yen" is changed to "dollar", the variable name "yen" will be a lie. In such a case, name the constant not based on what it is, but on what role and meaning it has. In the above example, for example, "priceSuffix" can be considered from the role of the suffix of the amount, or "currencyUnit" can be considered from the meaning of the currency unit.
Bad
var loadFlag = false
Avoid using flag as a Boolean variable name because the word flag only means that it is a Boolean and cannot express any purpose or meaning. The naming of Boolean variables has the following routine pattern, so it is good to follow this form.
--is + adjectives (isEmpty, etc.) --is + past participle (isHidden, etc.) --is + subject + past participle (isViewLoaded, etc.) --has + nouns (such as hasParent) --can + verb (canLoad, etc.) --Verbs (exists, contains, etc.) --should + verb (such as shouldLoad)
Related article [How to name the method and variable that return the boolean value](http://kusamakura.hatenablog.com/entry/2016/03/03/boolean_%E5%80%A4%E3%82%92%E8%BF % 94% E5% 8D% B4% E3% 81% 99% E3% 82% 8B% E3% 83% A1% E3% 82% BD% E3% 83% 83% E3% 83% 89% E5% 90% 8D % E3% 80% 81% E5% A4% 89% E6% 95% B0% E5% 90% 8D% E3% 81% AE% E4% BB% 98% E3% 81% 91% E6% 96% B9)
Reference information for successful method naming
When assigning variable names to things that you do not know the English name, I think that there are many cases where you first look up on the net, but be careful because machine translation such as Google Translate often does not translate words properly. When looking up English, it is better to look up not only Google Translate but also example sentences in dictionaries, Wikipedia, etc.
However, if all the team members are not good at English and it takes time to look up English, it is one way to give up English and write in Romanized Japanese. It's a bit clunky, but I think the important thing is the readability and productivity of all the team members.
Also, since the function name of the unit test is often a descriptive sentence, it is good to write it in Japanese if the environment can use Japanese for the function name.
Java unit test function name example
public void A test to see what happens when() {}
Avoid generic names as much as possible.
The most common are somehow Manager
and somehow Controller
.
Generic names tend to impose various processes and the class tends to grow.
When developing as a team, it is recommended to create a dictionary of terms used in the application and start development after matching the recognition of terms with the members. Creating a dictionary can prevent inconsistencies in which the same thing is defined by developers under different names, and eliminates the waste of individual developers having to worry about naming the same thing separately.
Related article Be careful of English part of speech when naming models and methods Class Naming Anti-Patterns
Code styles such as class and variable naming conventions should be unified and consistent within the project. Using exceptional names and styles for parts of a project can lead to poor readability and misunderstandings and oversights.
For example, if you already have the classes View001
, View002
, and View003
, when you add the View class next time, it is better to unify the naming method to View004
.
This goes against the "Don't use symbols or IDs in names" section above, but it's more important to have consistent names within your project.
If you have a problem with your current naming convention or style and want to change it, it's a good idea to get the consent of the team members and fix it all at once, not just some.
I prefer the form ʻobject.function ()that calls a function of an object to the form
function (object)` that passes an object to a function.
Bad
if StringUtils.isEmpty(string) {}
Good
if string.isEmpty() {}
There are several reasons for this and will be explained below.
The form function (object)
that passes an object to a function is difficult to read because parentheses are nested when multiple processes are repeated, but the form that calls an object function ʻobject.function ()` is connected by dots and multiple Good readability because it can be processed.
Bad
StringUtils.convertC(StringUtils.convertB(StringUtils.convertA(string)))
Good
string.convertA().convertB().convertC()
Generally, in the case of function (object)
, function is defined in some class or module, and in order to perform processing, two classes or modules in which function is defined and object as an argument are required.
On the other hand, in the case of ʻobject.function ()`, the reusability is high because the processing can be performed by the object alone.
** The form of object.function () is the essence of object orientation **.
When we talk about object-orientation, classes, inheritance, encapsulation, etc. are often explained first, but in reality they are not essential for object-orientation, and the only thing that is necessary for object-orientation is to call a method on an object. I think it's just the form of function () `.
Teaching object orientation to beginners is difficult. There seems to be a lot to teach about object orientation. However, forgetting about inheritance, encapsulation, polymorphism, etc., and letting the shape of ʻobject.function ()` soak into the body is the shortest route to acquire object orientation.
Related article [Object-oriented is one of the method dispatch methods](https://qiita.com/shibukawa/items/2698b980933367ad93b4#%E3%82%AA%E3%83%96%E3%82%B8%E3%82 % A7% E3% 82% AF% E3% 83% 88% E6% 8C% 87% E5% 90% 91% E3% 81% AF% E3% 83% A1% E3% 82% BD% E3% 83% 83 % E3% 83% 89% E3% 83% 87% E3% 82% A3% E3% 82% B9% E3% 83% 91% E3% 83% 83% E3% 83% 81% E3% 81% AE% E6 % 89% 8B% E6% B3% 95% E3% 81% AE1% E3% 81% A4)
Depending on the language, it may not be possible, but the function of Computed property allows function to be treated as property. Let's positively make the following functions Computed property.
--No arguments required --Returns a value --Processing is not heavy
Then, when it comes to the shape of ʻobject.function ()rather than
function (object)` at any time, it may not be the case.
You shouldn't take the form of ʻobject.function () if it gives the class of ʻobject
some unnecessary dependencies or roles that you shouldn't have.
The following example takes the form of ʻobject.function (), creating an unnecessary dependency on the View class for ʻenum APIResult
.
Bad
class LoginView: MyView {
//Receive the login result and move to the next screen
func onReceivedLoginResult(result: APIResult) {
let nextView = result.nextView() // object.function()Form of
showNextView(nextView)
}
}
enum APIResult {
case success
case warning
case error
func nextView() -> UIView {
switch self {
case .success: return HomeView()
case .warning: return WarningView()
case .error: return ErrorView()
}
//It depends on the HomeView, WarningView, and ErrorView classes.
}
}
In such an example, it would be better to take the form function (object)
as follows.
Good
class LoginView: MyView {
//Receive the login result and move to the next screen
func onReceivedLoginResult(result: APIResult) {
let nextView = nextView(result: result) // function(object)Form of
showNextView(nextView)
}
func nextView(result: APIResult) -> UIView {
switch result {
case .success: return HomeView()
case .warning: return WarningView()
case .error: return ErrorView()
}
}
}
enum APIResult {
case success
case warning
case error
}
The reason why you should not create a dependency like the former is explained in detail in the section "Awareness of the direction of dependence" below.
There is a problem with class inheritance. Inheritance is a powerful feature, but at the cost of many risks. Class inheritance is inflexible and vulnerable to change.
For example, if there are A and B classes that inherit from the Base class, and if you modify Base to fix the A function, the unrelated B function will be buggy. This can be said to be a problem with class design, but with inheritance such problems can occur.
Except for some languages such as C ++, multiple classes cannot be inherited. However, what actually exists often has a plurality of superordinate concepts (parent categories).
For example, suddenly, if you think about Indian curry
, its parents can be curry
or Indian food
.
class Indian curry extends curry
class Indian curry extends Indian food
To give an example that follows the implementation flow a little more, let's say that there is a system that allows you to make a smartphone contract on the WEB.
The contract screen is divided into two parts, a new contract
and a transfer (MNP) contract
, and there are classes corresponding to each.
Since there are many common parts between new contracts and transfer contracts, create a base contract
class as a common parent class.
Next, consider implementing a contract change screen.
If you create a new contract change
that inherits the new contract class and a transfer contract change
class that inherits the transfer contract class for contract change, it will not be possible to standardize the processing by inheritance from the viewpoint of contract change.
For example, if you create a class called BaseController
that is the parent class of all Controller classes, the BaseController class tends to be bloated because it incorporates the functions used by various Controllers.
The minimum required functionality is sufficient, but basically a common parent class should be prepared to be treated as a common interface rather than providing functionality.
Related article Disadvantages of inheritance I don't want to create something like BaseViewController in iOS app design Why is "synthesis" better than "inheritance" of a class? Improved code flexibility and readability in game development
Logic branching by if statement or switch statement tends to impair the readability of the program and cause bugs, so try to keep it as simple as possible.
If the nesting of if statements and for statements becomes deeper, the code becomes difficult to read, so do not make the nesting deeper as much as possible. To prevent deep nesting, it is good to perform "early return" and "logic cutout".
I will explain what happens when "early return" and "logic cutout" are applied to the following code using an example.
Before
if text != nil {
if text == "A" {
//Process 1
} else {
//Process 2
}
}
By returning the exceptional case first, the nesting of the main logic is made shallow. In the sample code below, the case where text is nil is returned first as an exception pattern.
After(Early return)
if text == nil {
return
}
if text == "A" {
//Process 1
} else {
//Process 2
}
However, as the name suggests, early return needs to be returned early. Basically, the function is expected to be processed up to the last line, so if there is a return in the middle of a long function, there is a risk that it will be overlooked and cause a bug.
Cut out the processing including nesting such as if statement and for statement into methods and properties, and make the nesting of the main logic shallow. In the sample code below, the part that determines whether text is "A" and performs some processing is cut out as a class method of the Text class.
After(Logic cutout)
if text != nil {
doSomething(text)
}
func doSomething(_ text: String?) {
if text == "A" {
//Process 1
} else {
//Process 2
}
}
Related information [Reduce the number of block nests](https://qiita.com/hirokidaichi/items/c9a76191216f3cc6c4b2#%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3 % 83% 8D% E3% 82% B9% E3% 83% 88% E3% 81% AE% E6% 95% B0% E3% 82% 92% E6% B8% 9B% E3% 82% 89% E3% 81 % 9D% E3% 81% 86)
Functions called from various places should not be classified by the caller. For example, in the following cases, processing is divided according to the screen, but in this writing method, the function becomes infinitely large as the number of screens increases.
Bad
class BaseViewController: UIViewController {
func doSomething() {
if viewId == "home" {
//Home screen processing
} else if viewId == "login" {
//Login screen processing
} else if viewId == "setting" {
//Setting screen processing
}
}
}
If you write this way, various processes are packed into one function, and there is a high possibility that it will become a huge function that is difficult to read, buggy, and difficult to fix.
Cases in functions like the one above can be resolved using polymorphism. A detailed explanation of polymorphism will be omitted because it will be long, but by overriding methods by interface (protocol) and inheritance, each case-specific process can be described in each child class.
class BaseViewController {
func doSomething() {}
}
class HomeViewController: BaseViewController {
override func doSomething() {
//Home screen processing
}
}
class LoginViewController: BaseViewController {
override func doSomething() {
//Login screen processing
}
}
class SettingViewController: BaseViewController {
override func doSomething() {
//Setting screen processing
}
}
Also, by passing a function as a function argument, it is possible to eliminate branches such as if statements. If the function is received as an argument as shown below, the processing of something can be freely set by the caller, so branching can be eliminated. In this example, it is a meaningless function that just executes the received function, but in many cases, completion processing and error processing that differ depending on the caller are passed as arguments.
class BaseViewController: UIViewController {
func doSomething(something: () -> Void) {
something()
}
}
The same thing can be achieved using interfaces in languages such as Java that cannot handle functions as objects.
To make the branch easier to see, try to reduce the number of lines in the branch block such as if statements as much as possible.
if conditionA {
//Don't write long processing here
} else if conditionB {
//Don't write long processing here
}
All methods should be either commands that perform actions or queries that return data, not both.
This is the idea called CQS (command query separation).
First, don't take action in a method (query) that returns data. In other words, it may be easier to understand if you do not update in the getter. I think many people keep this in mind as a matter of course.
Next, do not describe the query in the method (command) that executes the action as much as possible. This is more difficult to understand than the former, and it may be different from what the CQS head family says, but I will explain it with an example.
For example, consider a function that considers you logged in and goes to the next page if you have both a username and a session ID.
Bad
func nextAction() {
if userName.isNotEmpty && sessionId.isNotEmpty {
showNextPage()
}
}
If you think that the main purpose of this function is to "go to the next page", it will be a "command" instead of a "query", but the part that determines whether you are logged in will be a query that gets the value of Bool. , "Command" and "query" are mixed.
If the login judgment part is cut out from this function as a query to another function, it will be as follows.
Good
//command
func nextAction() {
if isLoggedIn() {
showNextPage()
}
}
//Query
func isLoggedIn() {
return userName.isNotEmpty && sessionId.isNotEmpty
}
If the conditional expression of the if statement is very short, it is difficult to cut it out to another function, but if it is a query of a certain length, it is better to cut it out from the action as another function or property.
As a guide, class should be about 50 to 350 lines and function should be about 5 to 25 lines. If it exceeds this, consider dividing the class or function.
The number of lines is just a guide, and it may be better to put more lines in the same class or function, but let's think that a class with more than 1000 lines is dangerous as a good upper limit.
This size is not the correct answer because it depends on the language, writing style, and function, but I will describe a rough sense of size for the number of lines in the class.
Number of lines | A feeling of size |
---|---|
Less than 50 lines | small |
Lines 50-350 | Appropriate |
350-700 lines | large |
700-1000 lines | Very big |
Over 1000 lines | Dangerous |
However, the underlying functions and UI classes with many elements may deviate from the above sense of size. For example, the Android View class has 27,000 lines, and in an extreme case, a screen with 1000 buttons that do different things would exceed 1000 lines, even if written briefly.
Related article [20 Articles for Writing Good Code for Intermediate Programming](https://anopara.net/2014/04/11/%E3%83%97%E3%83%AD%E3%82%B0 % E3% 83% A9% E3% 83% 9F% E3% 83% B3% E3% 82% B0% E4% B8% AD% E7% B4% 9A% E8% 80% 85% E3% 81% AB% E8 % AA% AD% E3% 82% 93% E3% 81% A7% E3% 81% BB% E3% 81% 97% E3% 81% 84% E8% 89% AF% E3% 81% 84% E3% 82 % B3% E3% 83% BC% E3% 83% 89 /)
It is close to the policy of not having instance variables as described in the section "Reducing the scope of variables", but try not to keep the Bool value flag more strictly in the instance variables. The information held by the flag (Bool variable) is the judgment result at a certain point in time, and is basically the past information. Therefore, there is a risk that it will deviate from the actual state as time goes by.
Bad
class Foo {
var hasReceived: Bool = false //Unnecessary flags
var data: Data?
func setData(data: Data) {
self.data = data
hasReceived = true
}
}
In the above example, whether or not data has been received is held by a flag, but the flag is not necessary because you can see that data has been received by looking at the data
variable.
Avoid creating such flags as it will result in multiple management of data.
In order to eliminate the flag, first consider whether it is possible to judge in another state instead of the flag. A flag determines some state, but it is often possible to determine the state by looking at something else without the flag.
If there are multiple flags, you can combine multiple flags into one Enum variable by creating an Enum that represents the state.
Bad
class User {
var isAdmin = false
var isSuperUser = false
var isGeneralUser = false
}
Good
enum UserType {
case admin //Administrator
case superUser //Superuser
case generalUser //General user
}
class User {
var userType: UserType = .admin
}
However, be sure to combine only one type of information into an Enum, and ** Do not combine multiple types of information into one Enum. ** ** In the above example, avoid adding "login status" to "user type" to create the following Enum.
Bad
enum UserLoginType {
case adminLoggedIn //Administrator (logged in)
case adminNotLoggedIn //Administrator (not logged in)
case superUserLoggedIn //Superuser (logged in)
case superUserNotLoggedIn //Superuser (not logged in)
case generalUserLoggedIn //General user (logged in)
case generalUserNotLoggedIn //General user (not logged in)
}
Avoid using numbers such as 0, 1, 2 to determine conditional branching. Alternatives vary from case to case, but arrays and Lists often avoid the use of numbers.
Bad
func title(index: Int) -> String {
switch index {
case 0:
return "A"
case 1:
return "B"
case 2:
return "C"
default:
return ""
}
}
Good
let titles = ["A", "B", "C"]
func title(index: Int) -> String {
return titles.indices.contains(index) ? titles[index] : ""
}
However, 0, 1, and -1 often have special roles such as getting the first element or indicating an API error or success, so you may have to use them depending on the environment.
Statically typed languages do not use arrays (Array, List) to pass multiple types of data.
The following function uses numbers to store the first name, second zip code, and third address information in the List, but it is possible to determine what information is in what number from the List type. Because it cannot be done, readability is poor and mistakes are likely to occur.
Bad
func getUserData(): [String: String] {
var userData: [String] = []
userData[0] = "Masao Yamada"
userData[1] = "171-0001"
userData[2] = "Toshima-ku, Tokyo"
return userData
}
When passing a fixed data structure, create a struct or class as shown below and pass the data.
Good
struct UserData {
let name: String
let postalCode: String
let address: String
}
func getUserData(): UserData {
return UserData(name: "Masao Yamada",
postalCode: "171-0001",
address: "Toshima-ku, Tokyo")
}
However, if you want to pass a list of names, a list of files, or a list of the same type of data, you can use an array or List.
For exactly the same reason, avoid passing multiple types of data in Map (Dictionary). If it is a statically typed language, create a struct or class for data transfer as in the array example.
Bad
func getUserData(): [String: String] {
var userData: [String: String] = [:]
userData["name"] = "Masao Yamada"
userData["postalCode"] = "171-0001"
userData["address"] = "Toshima-ku, Tokyo"
return userData
}
Delete the code you no longer need without commenting it out. It's okay to comment out temporarily during a local fix, but basically don't commit what you comment out.
If the number of commented out lines increases, the code becomes difficult to read, and unused parts are caught during the search, which is quite harmful. You can see the history of deletions and changes with management tools such as Git, so try to delete unnecessary code.
Set rules for dependencies between programs to prevent unplanned dependencies. It's an abstract explanation, but it depends on the following directions and avoids this opposite direction.
Some programs rely on some other class, module, library, etc. that they can't compile or work without it. In addition to the dependency on the compile level, it can be said that it depends on the specification even if it is created on the premise of a specific specification and does not work without that specification.
General-purpose classes should not depend on dedicated classes.
For example, in an extreme case, if the String
class (general purpose function) that represents a string depends on the login screen class LoginView
(dedicated function), all systems that use String will be LoginView. It becomes necessary to take in wastefully, and it becomes difficult to reuse the String class.
To give another example that is more common, if there is a HTTPConnection
class (general-purpose function) for communication processing, the screen class (dedicated function) unique to the application is processed in this class. If you use it for judgment, you will not be able to port the HTTPConnection
class to another app.
Avoid using the application's own dedicated screen class or ID for judgment in the general-purpose function class because it may cause excessive branching in the general-purpose class and complicate it.
In modern times, it is natural to use some kind of open source library to implement an application, but the processing using a specific library is combined into one class or file, and various classes do not depend on the library. It's an image that depends not on the surface but on the point.
By consolidating the code using the library in one place, the impact and correction of the following changes can be minimized.
--I want to replace the library I'm using with another library --Specifications have changed in the library and usage has changed.
Although it suffers from what I wrote in the section "Depending on general-purpose functions from dedicated functions", classes that aim to retain data such as DTO should be as simple as possible and depend on other functions. , It is better not to depend on a specific specification.
//Simple data class example
struct UserInfo {
let id: Int
let name: String
}
Such a simple data class can be used across many layers and sometimes ported to another application, but extra dependencies can be detrimental.
In many statically typed languages, interfaces (protocols) can be used to eliminate dependence on specific implementation classes. (This section often does not apply to dynamically typed languages because they often do not have an interface.)
For example, in the example below, the ʻExampleclass depends on the
HTTPConnector` class with which it communicates.
Before
class Example {
let httpConnector: HTTPConnector
init(httpConnector: HTTPConnector) {
self.httpConnector = httpConnector
}
func httpPost(url: String, body: Data) {
httpConnector.post(url: url, body: body)
}
}
class HTTPConnector {
func post(url: String, body: Data) {
//Implementation of HTTP communication
}
}
If the HTTPConnector
class uses some kind of communication library, the ʻExampleclass will indirectly depend on that communication library. However, if you change
HTTPConnector to an interface (protocol) as follows, you can make the ʻExample
class independent of the communication library.
After
class Example {
let httpConnector: HTTPConnector
init(httpConnector: HTTPConnector) {
self.httpConnector = httpConnector
}
func httpPost(url: String, body: Data) {
httpConnector.post(url: url, body: body)
}
}
protocol HTTPConnector {
func post(url: String, body: Data)
}
Code related to cooperation with systems outside the application and interface should be put together in one place as much as possible. Various external systems can be considered, but the most common ones are HTTP APIs and databases.
It's the same as I wrote about dependence on open source libraries, but by consolidating the integration with external systems in one place, the impact and modification of changes in the external system can be minimized. Again, it would be nice to use an interface (protocol) to eliminate the dependency on a specific implementation class.
Enum is defined for numeric constants, string constants, code values, etc. that have multiple values.
Bad
func setStatus(status: String) {
if status == "0" {
//Processing on success
}
}
Good
enum Status: String {
case success = "0"
case error = "1"
}
func setStatus(status: Status) {
if status == .success {
//Processing on success
}
}
Enums can only handle predetermined values, but some processing may be required for unexpected values such as API status code. In such a case, it is better to keep the raw code value as follows so that you can get the Enum with getter or computed property.
struct Response {
var statusCode: String
var status: Status? {
return Status(rawValue: statusCode)
}
}
func setResponse(response: Response) {
if response.status == .success {
//Processing on success
} else if response.status == .error {
//Processing at the time of error
} else {
//If an unexpected code value arrives, the code value is output as a log.
print("Status code: \(response.statusCode)")
}
}
Write comments on code that is difficult for a third party to see. For code that doesn't immediately make sense when read by others, such as code that meets special specifications or tricky code that fixes bugs, comment out the code description.
There are times when you can't fix a bug in a straightforward manner and you have no choice but to put in dirty code. If this is the case, leave a comment as to why you did so for later viewers.
For example, in the following cases, it is better to write an explanation in the comments.
--If you execute it normally, it will not work well, so execute it with a momentary delay. --For some reason, it doesn't work, so assign the value to the same property twice. --Create a Boolean property to process special cases separately --Although it has nothing to do with the requirements, there was a systematic problem, so the if statement is used to separate cases.
The code below needs to be commented because no third party knows what 3
means.
Bad
if status == 3 {
showErrorMessage()
}
Good
//Status 3 is a communication error
if status == 3 {
showErrorMessage()
}
Japanese is rather roundabout, and writing sentences without thinking usually contains some words that are not informative. After writing a sentence, review it once, check for duplicate words or unnecessary words, and if there are, delete them and shorten them.
For example, the following words can be simplified and shortened without changing their meaning.
―― "Can be shortened" → "Can be shortened" --"Modify to adjust ~" → "Adjust ~"
In Japanese, the more polite the wording, the more characters there are. "I did" has 2 characters, while "I did" has 4 characters. There is a matter of taste, but if you don't need it, you can shorten the comment by not using polite or honorifics.
Try to write simple and easy-to-understand sentences without using difficult words, phrases, kanji, and technical terms. Note that foreign words in katakana are often difficult to convey to English-speaking people.
Also, it is best not to use technical terms as much as possible, but it is okay to use the ones necessary to explain the program. For example, the word "Socket communication" is a technical term, but it may be necessary to use it in the comments of programs related to "Socket communication".
If the project involves people of various nationalities, it is good to unify the comments in English.
The most important thing is that it is easy for the reader to understand. Extreme story, if the reader can understand ** It doesn't matter if the grammar is wrong or the English is wrong. ** ** You don't have to spend time writing beautiful sentences again, and Frank is fine, so it's important to write comments that are useful to the reader.
The following suspicious comments may be better than not just information.
//There is a memory leak here.
//The branch here may not make much sense. ..
//I don't know why it works, but for the time being, this code solved the problem here.
As a general premise, ** the app should not fall **.
It may seem obvious, but modern programming languages can easily crash. For example, many languages will crash if you access the 4th in an array with 3 elements. This kind of bug is easy to occur, and it is not uncommon for some to be released unnoticed.
There is a good idea that you should throw an Exception if it's in a bad state, but it's not a good idea to crash the entire application.
In back-end programs such as APIs and batches, Exceptions rarely stop the entire system, so it is good to actively utilize Exceptions, but in front-end applications, one Exception completely stops the application. Since it often happens, it is better not to throw an Exception as much as possible.
In order to prevent such a crash as much as possible, the following measures are required.
Add an if statement etc. to the code that may crash, and if it crashes, do not process it. This includes null checking in non-null-safe languages like Java.
Example of null checking in Java
if (hoge != null) {
hoge.fuga();
}
However, since such guards have the risk of squeezing and hiding bugs, it is better to use an assert mechanism that stops processing when fraudulent occurs only in the development environment. For example, Swift's ʻassert` function can only check conditions and crash if it's invalid, only for DEBUG builds.
Assert example in Swift
func hoge(text: String?) {
assert(text != nil) //DEBUG build crashes here if text is nil
//Normal processing
}
Or, even if you put a guard in the if statement, it is better to output at least a log if an invalid state occurs.
If possible, create a development environment where crashing code is hard to write in the first place. This is a more radical solution than implementing guards. For example, one fundamental solution to NullPointerException is to "quit Java and use NULL-safe Kotlin".
You can also add extensions to write code safely, such as adding an Optional class to a language that is not null-safe, without having to change the language.
The following is an example of extending Swift's Collection and adding a getter (subscript) that returns nil
without crashing even if an index outside the range is specified.
extension Collection {
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
let list = [0, 1]
print(list[safe: -1]) //Become nil without crashing
Common and common crashes such as NULL access and array out-of-range access can be dealt with by adding such functions.
Even if a part of the data has an invalid state or an unexpected state, the part that does not have a problem should be processed as usual as much as possible. For example, consider a system that allows you to view bank account details.
Comparing a system in which nothing can be seen if one of the usage statement data is incorrect and a system in which the account balance can be viewed with the other details even if one of the usage statement data is incorrect, the latter is naturally more convenient for the user.
Ideally, your application should behave like a good butler who supports your users (at least I think so). It is far from a good butler to abandon all duties just by one unexpected thing.
** However, this policy applies only to the ability to view information. ** ** In the case of irreversible update processing, it is safe to cancel the processing as soon as an abnormality is detected.
By combining the same processes written in multiple places into one, it is possible to reduce the amount of code, improve readability, and reduce the cost of modification.
General-purpose functions that are not affected by business requirements are cut out and used as common classes and functions. For example, the following defines the function of URL-encoding a character string as a common function.
extension String {
var urlEncoded: String {
var allowedCharacterSet = CharacterSet.alphanumerics
allowedCharacterSet.insert(charactersIn: "-._~")
return addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? ""
}
}
When branching a process with an if statement or switch statement, if the same process exists in the branch, the duplicated part should be outside the if statement or switch statement.
For example, in the following, the label.text =
part is duplicated in both the if block and the else block.
Bad
if flag {
label.text = "aaa"
} else {
label.text = "bbb"
}
In this case, the if statement itself can be erased by removing the duplicated part.
Good
label.text = flag ? "aaa" : "bbb"
There are also disadvantages to standardizing logic and data structures. Unnecessarily sharing the same process indiscriminately often leads to a decrease in maintainability.
There are the following disadvantages to standardizing the processing
In particular, commonality by class inheritance creates strong tight coupling and may reduce the flexibility of the program, so be careful.
Related article Speaking sleepy saying "Let's inherit to make the code common" The idea that commonality only creates anti-patterns
Then, when should it be common and when should it not be common?
As mentioned at the beginning, we will actively aim to standardize general-purpose functions that are not affected by business requirements. In the case of logic and data related to business requirements, it is a case-by-case decision whether to standardize or not, but satisfying the following is a guideline for standardization.
Just as you make wireframes and rough sketches first when you create an application, programs can proceed efficiently by making wireframes and rough sketches before you start writing code.
When development starts, we want to make visible progress, so we tend to start writing code for the time being, but code written without thinking deeply often causes rework due to omission of consideration, inadequate specifications, and inadequate design. If you write the code for the time being, it looks like it's progressing at first glance, but the bad code is often ** totally useless or even harmful ** in the end.
Therefore, it is more efficient to put up with the desire to write code at first and make a rough sketch of the program from the perspective of the entire project.
The rough sketch of the program mentioned here assumes a class diagram and outline code. However, this is not always the case, and another form may be used if the following is known.
At first, it is easy to understand if you make a class diagram to consider the inclusion relations and reference relations between classes. The class diagram at this stage does not require any rules, and you can sketch as you like. It is not necessary to cover the properties and functions of the class, and it is only necessary to know the class name, inclusion relation, reference relation and role.
It is recommended to make it by hand because it will be rewritten through trial and error. It is easier to understand if you write multiple N sides of a pair of N or enclose the inclusion with a line to make the UML diagram simpler and more intuitive.
Once the structure of the class is decided, the next step is to decide the interface between the data held by each class and the function required for cooperation with the outside.
The data to be retained is specifically instance variables (member variables). Unlike the class diagram, try to cover the instance variables as much as possible at this stage.
As mentioned in the first section, the instance variables of the class should be as small as possible. Make only the necessary instances variables, and try not to add unplanned instance variables after designing.
Once the class structure is decided, write rough code without details. However, since the purpose here is to decide the outline, write in a way that is easy for you to understand without being bound by detailed grammar. Therefore, it is recommended to write in a simple editor rather than an IDE that checks for compilation errors.
In rough code, it is sufficient if the processing flow, how to call other classes, and the interface for passing information are roughly determined.
Rough code example
func createSession(request) -> Session? {
let user = userTable.getByUserName(request.userName)
if user.Account is locked{
throw some error
}
if user.passward != request.password {
userTable.Count up the number of login failures
return nil
}
Log output of logged-in user information
return Session()
}
The interface allows you to implement the business logic that forms the backbone of the detailed implementation and pending issues. In other words, you can compile and create rough code that can be used in the product as it is. This method works very well for backend programs with DI containers.
The following is an example of rough code (language is Java) that utilizes the interface with Spring boot that has a DI container.
This program uploads a photo to a file server and emails the URL of the file to the user's email address obtained from the DB. By using the interface, you can implement business logic without deciding the database, file server, mail server, and library to use.
Java interface utilization example
@Service
class FileService {
private final IMailer mailer;
private final IUserTable userTable;
private final IFileUploader fileUploader;
//In Spring, the value is automatically set from the DI container to the argument of the constructor.
public FileService(IMailer mailer, IUserTable userTable, IFileUploader fileUploader) {
this.mailer = mailer;
this.userTable = userTable;
this.fileUploader = fileUploader;
}
void uploadUserPhoto(int userId, byte[] photoData) {
String fileURL = fileUploader.upload(userId, photoData);
User user = userTable.get(userId);
mailer.send(user.emailAddress, "Upload completed", fileURL);
}
}
interface IMailer {
void send(String toAddress, String title, String body);
}
interface IUserTable {
User get(int userId);
}
interface IFileUploader {
String upload(int userId, byte[] fileData);
}
If you make a prototype of the system by putting a simple mock implementation in such an interface, you can develop flexibly and quickly by ignoring the DB and server for the time being.
Finding deficiencies in the specifications is also one of the purposes of making rough sketches.
When writing a program based on specifications and design documents, it is necessary to keep in mind that ** specifications and design documents are always incorrect **. If you notice a mistake in the specifications after implementation, the work up to that point will be wasted.
Therefore, when writing a rough sketch, be aware that there are no deficiencies in the specifications.
Placing related code in the same directory or in the same file saves you the trouble of finding and switching files when reading code. For example, put files used by the same function in the same directory, or make a class used only in a specific class an inner class, and place related code nearby to make it easier to find the code.
When grouping files into directories or packages, there are two methods, one is to group them by function and the other is to group them by file type.
Storyboard
├ LoginViewController.storyboard
└ TimeLineViewController.storyboard
View
├ LoginView.swift
└ TimeLineView.swift
Controller
├ LoginViewController.swift
└ TimeLineViewController.swift
Presenter
├ LoginViewPresenter.swift
└ TimeLineViewPresenter.swift
UseCase
├ LoginViewUseCase.swift
└ TimeLineViewUseCase.swift
Login
├ LoginViewController.storyboard
├ LoginView.swift
├ LoginViewController.swift
├ LoginViewPresenter.swift
└ LoginViewUseCase.swift
TimeLine
├ TimeLineView.swift
├ TimeLineViewController.storyboard
├ TimeLineViewController.swift
├ TimeLineViewPresenter.swift
└ TimeLineViewUseCase.swift
Both examples above have the same files, but the directories are divided differently.
The method of grouping by file type is easy to understand the layer design, and files used by multiple functions can be arranged without contradiction, but it is complicated because it is necessary to search for files across 5 directories in order to read the code on one screen. There are also disadvantages.
It is not the best way to divide these two methods, and it is necessary to use them properly according to the situation, but if the files are not used by other functions, it may be easier to develop by grouping directories according to the function. There are many.
In general, loosely coupled programs are more versatile and maintainable, but the policy in this section conversely increases the degree of program coupling. If the degree of coupling is too high, one file will become too large or tightly coupled, which will impair versatility and maintainability, but unnecessary and excessive loose coupling also has the disadvantage that the cost will outweigh the advantages. ..
What is best is an issue that has no correct answer on a case-by-case basis, but it is necessary to be aware of putting the code together in an appropriate balance.
UnitTest here is not a manual one, but a program test such as JUnit. Actively create Unit Tests from the early stages of development for small functions such as data processing.
If a problem is found in the system, the larger the function, the longer it takes to investigate the cause. In addition, the later the process, the greater the influence of program modification on other functions and the points that must be taken into consideration. These costs can be reduced by checking the operation of small functions with UnitTest at an early stage.
Processing on irregular data and states is often difficult to test by actually running the application. Even in such tests, UnitTest allows you to programmatically create irregular situations and test them.
By writing a UnitTest, the programmer will actually use the class or function under test in the program. By experiencing the usability of classes and functions at this time, you can brush up the interface of classes and functions in a more user-friendly form. Also, as a side effect, it may be a study of clean design because the program is required to be loosely coupled to execute UnitTest.
You need to determine what is right to make a test. By considering the test, the specifications and issues such as what should be done in the irregular case become clearer.
UnitTest is done not to guarantee quality, but to speed up overall development.
Therefore, it is not necessary to cover all the code, and it is not necessary to force a test that is difficult to implement. Especially when UI, communication, DB, etc. are involved, UnitTest needs various consideration, so there is a risk that development efficiency will decrease if such UnitTest is created.
Also, you should stop thinking that quality is guaranteed because you did UnitTest. No matter how much you do Unit Test, you will eventually need to do a manual test.
UnitTest is basically done for small and independent functions. Sometimes you create a program that automatically checks a large series of operations, but that has a different purpose than Unit Test.
Do not use basic data types such as Int, Float, and Double for business logic calculations, but use numeric classes such as BigDecimal for Java and NSDecimalNumber and Decimal for Swift.
Floating-point numbers such as Double and Float should not be used easily as they cause errors. Floating point is generally used only for drawing processing such as coordinate calculation and scientific calculation that emphasizes performance. If you make a mistake, don't use Double or Float to calculate the amount.
For example, Java Int is 32bit and the maximum value is about 2.1 billion, which is too small to handle the amount of money. 2.1 billion yen is a lot of money, but if you are accounting at the company level, the amount that exceeds this is normal, and it is not an amount that can not be a personal asset.
Since built-in integer types such as Int have an upper limit on the number of digits, avoid using them in business logic as much as possible, and when using them, carefully consider whether there are cases where digits overflow. For monetary amounts, using a 64-bit integer value (Long type for Java) should be almost sufficient. The maximum value of a 64-bit integer value is about 900 K, so even 100 times the US national budget (about 400 trillion yen) can be easily accommodated.
Related article Why should I use BigDecimal
You should try not to make the class too large, but there are the following disadvantages to dividing the class excessively and increasing the number of classes and interfaces too much.
--Code is required for class combination, increasing the total amount of code --The relationships between classes become complicated and the code becomes difficult --File switching is required when reading and editing code, which reduces work efficiency.
When designing a class or layer structure, it is necessary to consider these disadvantages and design the advantages outweigh the disadvantages.
When cutting out any function into a class, the function is likely to be one of the following.
--Reusable (universal) --Need to be tested alone --Need to deploy (release) independently
On the contrary, it is highly possible that it is better to provide a group of functions that are not reusable and are not tested or deployed independently as one class without dividing them into classes. Loosely coupling functions helps maintain system maintainability, but there are costs and disadvantages, and loosely coupled is not always better than tightly coupled.
For example, in an iOS app, there is a design pattern that creates a ViewController
and a customView
for a specific screen, but such ViewController and View are always linked one-to-one and are not reusable. , Since it is not tested or deployed independently, it is often better to combine it into one class instead of dividing it into View and ViewController.
Excessive class subdivision is often the result of mechanical imitation of well-known design patterns such as DDD and clean architecture. Books and articles that introduce design patterns tend to emphasize only the good points of design patterns and not touch on the bad points or troublesome points. Optimal design depends on the size of the product and team, so instead of mechanically imitating the existing design pattern, it is enough to apply it to our product and see if the advantages outweigh the disadvantages. consider. However, it is difficult to understand the disadvantages of a particular design pattern without trying it once. Optimal design patterns need to be gradually refined through the PDCA cycle of review, execution, feedback, and improvement.
For example, it is useless to apply many classes and complex architectures to a program that only outputs Hello World. There's not much to do, so there are classes and layers that don't do anything.
The appropriate class or layer to prepare depends on the nature and complexity of the functionality to be implemented. ** There is no convenient class structure or architecture that matches anything. ** **
A typical application has multiple screens and functions, each with different properties and complexity, so applying the same layer structure to all of them would result in waste and mismatch. Therefore, instead of applying the same configuration to all functions, it is better to be more flexible and select an appropriate design for each function.
Even if the functions are the same, the best configuration will change as the requirements and specifications change. Bold refactoring and even throwing away existing code altogether are sometimes necessary to maintain a good design.
One way to reduce the number of files in a layered architecture, such as a clean architecture, is to allow layer shortcuts. For example, even if the program has the following layer structure, if there is nothing to do with UseCase and Presenter, the Controller can directly refer to the Entity.
As mentioned in the previous section, it is not necessary to unify the layer structure within the application, and I think that you can change it more freely depending on the function. However, as described in the section "Be aware of the direction of dependence", do not refer to the View in the opposite direction from the Entity.
In a language that can pass a function as an argument, such as a functional language, a simple interface (protocol) that has only one method can be replaced with a function pass or closure. The following replaces Presenter's Output from protocol to function passing.
Before
class LoginPresenter {
let output: LoginOutput
init(output: LoginOutput) {
self.output = output
}
func login() {
output.loginCompleted()
}
}
protocol LoginOutput {
func loginCompleted()
}
After
class LoginPresenter {
func login(onComplete: () -> Void) {
onComplete()
}
}
If you are too obsessed with layer separation, you may end up with multiple similar data models meaninglessly.
For example, there is a ʻUser class in the domain layer, a ʻUserDTO
to pass that information to UseCase, and a ʻUserViewModel` to use in a View, but the three have almost the same code. It's a case.
In some cases, even if they are almost the same, they must be separated, but in other cases, they can be reused without being separated.
If the following conditions are met, the upper layer data model may be used across multiple layers.
--The data model does not depend on a specific architecture such as DB, framework, library, etc. --The upper data model does not depend on the lower class (as described in "Awareness of Dependency")
Basically, Exception is thrown to the caller without catching, and error handling is performed collectively in the upper layer. When catching an Exception, keep the following in mind.
--Handle appropriate errors --If error handling is not possible, output an error log --If you have a clear intention and hide the Exception without even logging, state the reason in the comment.
The content in this section is a bit inconsistent with what is in the "Awareness of Service Availability" section, but it's best if the upper layers can handle Exceptions properly.
The policy described in the "Awareness of Service Availability" section is to avoid throwing Exceptions if the upper layers may not be able to handle Exceptions properly, which may lead to a system-wide crash. Thing.
Although the exception is handled in the upper layer, let's stop using the exception for conditional branching like the if statement in the normal control flow. Exceptions are just exceptions, and you should try not to use them except in exceptional situations.
The Java standard Exception becomes a checked exception (checked exception), and throw
forces the calling function to either catch or rethrow.
Therefore, if you try to handle the exception at the top layer, you will need to add throws
to the expression that calls the function that raises the exception.
Sometimes this is annoying, but if you don't want to catch and hide the exception, you can wrap it in a RuntimeException
and throw it without making any changes to the caller.
try {
"ABC".getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
//IllegalStateException is a RuntimeException so don't force catch on top
throw new IllegalStateException(e);
}
Properties and functions that are not used from outside the class should be private or protected.
It is desirable that you can understand how to use a class by code completion without looking at the code or documentation, but if you do not make it private or protected, properties and functions that are not premised to be used from the outside will also appear as candidates in code completion, making it difficult to use. ..
However, some programming languages do not have access modifiers such as private, in which case it is better to give up private by giving priority to simple and standard code rather than realizing private by a special implementation. If you really want to introduce private, it is recommended to change the language in the first place (such as changing from JavaScript to TypeScript)
In addition, since the implementation of private methods is often neat by cutting out the processing to public methods of another class, consider separating the classes if it is a general-purpose processing or the class size is large.
Related article No private method required
Avoid nesting ternary operators as they are confusing.
Bad
flag ? subFlag ? a : b : c
When nesting is necessary, it is advisable to put the operation result in the middle into a local variable (explanatory variable) to eliminate the nesting.
Good
let aOrB = subFlag ? a : b
flag ? aOrB : c
Also, if a long expression or a chain of long functions is included in the ternary operator, put each result in a local variable and then use it in the ternary operator.
Bad
flag ? (1 + 2) * 3 - 4 : 5 % 6 * 7
Good
let a = (1 + 2) * 3 - 4
let b = 5 % 6 * 7
flag ? a : b
Related article Ternary operator?: Is evil.
Beginners tend to write true`` false
in their code as follows.
Bad
var isZero: Bool {
if number == 0 {
return true
} else {
return false
}
}
However, in the above, since number == 0
returns Bool, it is not necessary to write true / false, and it can be written more simply as follows.
Good
var isZero: Bool {
return number == 0
}
In cases where true is always returned or false is always returned, it is necessary to write true / false in solid, but when returning some judgment result as in the above example, avoid writing true / false in solid.
However, this writing method is difficult for beginners to understand. If you have a beginner in your team, it's a good idea to explain.
Related article I want to write a reader-friendly if statement
Like Swift's enum rawValue, enums often have some code value. However, if this code value is used for judgment etc., the significance of the existence of enum will be halved.
Enum sample
enum Status: String {
case success = "0"
case error = "1"
}
For example, if there is an Enum with a code value as described above, use the Enum directly instead of using the code value for judgment.
Bad
if status.rawValue == "0" {
}
Good
if status == .success {
}
Even when passing as an argument, the code value is not used and Enum is passed directly.
Bad
checkStatus(status.rawValue)
Good
checkStatus(status)
This is inconsistent with the section "Set code value to enum", but if enum does not have a code value in the first place, such a mistake does not occur.
Considering that the code values of DB and API are external specifications and should be separated from the application specifications, it is better to convert the code values to enums in the Repository
layer and not give the enums code values. become.
Actively utilize static code checking such as Lint.
Static code checking allows you to check the code at low cost without human intervention, so it is better to do it. Not only can you detect problems, but you can also learn what kind of code has a problem.
However, static code checking also has its disadvantages, and when a large number of warnings are issued, it may take a very long time to check everything seriously. Just because a warning is issued does not mean that there is an immediate problem, so it is possible that the quality of the application does not change much even if the warning is crushed over time.
It may be better to think of static code checking only to help find problems.
I won't go into detail, but list other miscellaneous practices.
<
and not >
for size comparisonWhat did you think? This article is the coding guideline of Alto Notes Co., Ltd., which I represent.
We are a company of 2 people including the representative, but we are currently looking for iOS / Android engineers. If you are interested, please take a look at the job information!
Recommended Posts